fix: deliver Vodafone UK MT SMS over WiFi Calling (#39)

- Read ipsec-3gpp UDP server port even when REGISTER used TCP
- Advertise Allow MESSAGE and smsip Accept-Contact for all carriers
- Do not require P-CSCF port-c (50601) on inbound UE-server XFRM
- Log rejected inbound TCP/UDP sources

Verified on VOXI/Vodafone UK 23415 with Quectel EC25.

Closes #38

Co-authored-by: wl77vv <[email protected]>
This commit is contained in:
Lian Wang
2026-08-16 22:24:33 +08:00
committed by GitHub
co-authored by wl77vv
parent bfda29193a
commit fa8afb9571
3 changed files with 62 additions and 38 deletions
+7 -1
View File
@@ -946,7 +946,7 @@ func (session *Session) buildRegister(
}
o2Germany := usesO2GermanyIMSProfile(session.request.Identity)
supported := "path, gruu"
allow := "REGISTER, INVITE, ACK, CANCEL, BYE, OPTIONS"
allow := "REGISTER, INVITE, ACK, CANCEL, BYE, OPTIONS, MESSAGE, SUBSCRIBE, NOTIFY"
if o2Germany {
// Match the complete IMS capability set used by the previously working
// VoHive client. O2 validates more of the initial UE security profile
@@ -987,6 +987,12 @@ func (session *Session) buildRegister(
"Accept-Contact: *;+g.3gpp.smsip",
`Accept-Contact: *;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel"`,
)
} else {
lines = append(lines,
"P-Access-Network-Info: IEEE-802.11;i-wlan-node-id=000000000000;network-provided",
"Accept-Contact: *;+g.3gpp.smsip",
`Accept-Contact: *;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel"`,
)
}
if session.securityOffered() {
lines = append(lines,
+43 -33
View File
@@ -534,25 +534,7 @@ func buildXFRMInstallPlan(config IPSecSAConfig) ([]xfrmOperation, error) {
for _, protocol := range flow.protocols {
operations = append(operations, xfrmOperation{
description: flow.description + " " + protocol + " policy",
arguments: []string{
flow.family,
"xfrm", "policy", "add",
"src", flow.sourcePrefix,
"dst", flow.destinationPrefix,
"proto", protocol,
"sport", strconv.Itoa(flow.sourcePort),
"dport", strconv.Itoa(flow.destinationPort),
"dir", flow.direction,
"priority", "100",
"tmpl",
"src", flow.templateSource.String(),
"dst", flow.templateDestination.String(),
"proto", "esp",
"spi", fmt.Sprintf("0x%08x", flow.spi),
"reqid", strconv.FormatUint(uint64(flow.reqid), 10),
"mode", "transport",
"level", "required",
},
arguments: xfrmPolicyArgs(flow, protocol, false),
})
}
}
@@ -568,16 +550,7 @@ func buildXFRMCleanupPlan(config IPSecSAConfig) []xfrmOperation {
protocol := flow.protocols[protocolIndex]
operations = append(operations, xfrmOperation{
description: "delete " + flow.description + " " + protocol + " policy",
arguments: []string{
flow.family,
"xfrm", "policy", "delete",
"src", flow.sourcePrefix,
"dst", flow.destinationPrefix,
"proto", protocol,
"sport", strconv.Itoa(flow.sourcePort),
"dport", strconv.Itoa(flow.destinationPort),
"dir", flow.direction,
},
arguments: xfrmPolicyArgs(flow, protocol, true),
})
}
}
@@ -606,6 +579,45 @@ func buildXFRMCleanupPlan(config IPSecSAConfig) []xfrmOperation {
return operations
}
func xfrmPolicyArgs(flow xfrmFlow, protocol string, delete bool) []string {
args := []string{
flow.family,
"xfrm", "policy",
}
if delete {
args = append(args, "delete")
} else {
args = append(args, "add")
}
args = append(args,
"src", flow.sourcePrefix,
"dst", flow.destinationPrefix,
"proto", protocol,
)
if flow.sourcePort > 0 {
args = append(args, "sport", strconv.Itoa(flow.sourcePort))
}
if flow.destinationPort > 0 {
args = append(args, "dport", strconv.Itoa(flow.destinationPort))
}
args = append(args, "dir", flow.direction)
if delete {
return args
}
return append(args,
"priority", "100",
"tmpl",
"src", flow.templateSource.String(),
"dst", flow.templateDestination.String(),
"proto", "esp",
"spi", fmt.Sprintf("0x%08x", flow.spi),
"reqid", strconv.FormatUint(uint64(flow.reqid), 10),
"mode", "transport",
"level", "required",
)
}
type xfrmFlow struct {
description string
family string
@@ -650,7 +662,7 @@ func xfrmFlows(config IPSecSAConfig) []xfrmFlow {
{
description: "P-CSCF-client to UE-server", family: family,
sourcePrefix: remotePrefix, destinationPrefix: localPrefix,
sourcePort: config.PCSCFClientPort, destinationPort: config.UEServerPort,
sourcePort: 0, destinationPort: config.UEServerPort,
direction: "in", templateSource: config.RemoteIP, templateDestination: config.LocalIP,
spi: config.UEServerSPI, reqid: serverPairReqID(config),
protocols: []string{"tcp", "udp"},
@@ -916,9 +928,7 @@ func (session *Session) validProtectedUDPSource(remote *net.UDPAddr) bool {
return false
}
expectedIP := addressIP(session.conn.RemoteAddr())
return expectedIP != nil &&
expectedIP.Equal(remote.IP) &&
remote.Port == session.securityAgreement.selected.portClient
return expectedIP != nil && expectedIP.Equal(remote.IP)
}
func (session *Session) effectiveSecurityMode() string {
+12 -4
View File
@@ -91,11 +91,14 @@ func (session *Session) startRuntimeReceivers() error {
session.receiveDone.Add(1)
go session.readMainConnection()
if session.securityActive && session.transport == "tcp" && session.protectedTCP != nil {
// Vodafone UK (and others) deliver MT SMS as SIP MESSAGE to the
// ipsec-3gpp UE server port over UDP even when REGISTER used TCP.
// Always read both sockets when they were reserved.
if session.securityActive && session.protectedTCP != nil {
session.receiveDone.Add(1)
go session.acceptProtectedTCP()
}
if session.securityActive && session.transport == "udp" && session.protectedUDP != nil {
if session.securityActive && session.protectedUDP != nil {
session.receiveDone.Add(1)
go session.readProtectedUDP()
}
@@ -140,6 +143,8 @@ func (session *Session) acceptProtectedTCP() {
return
}
if !session.validProtectedTCPSource(connection.RemoteAddr()) {
session.logInboundSMS(slog.LevelWarn, "IMS inbound TCP rejected", nil,
"stage", "source_filter", "remote", connection.RemoteAddr().String())
_ = connection.Close()
continue
}
@@ -186,6 +191,8 @@ func (session *Session) readProtectedUDP() {
return
}
if !session.validProtectedUDPSource(remote) {
session.logInboundSMS(slog.LevelWarn, "IMS inbound UDP rejected", nil,
"stage", "source_filter", "remote", remote.String(), "packet_bytes", count)
continue
}
packet, err := parseSIPPacket(buffer[:count])
@@ -208,8 +215,9 @@ func (session *Session) validProtectedTCPSource(address net.Addr) bool {
return false
}
expected := addressIP(session.conn.RemoteAddr())
return expected != nil && expected.Equal(remote.IP) &&
remote.Port == session.securityAgreement.selected.portClient
// Require P-CSCF IP. Do not require port-c (50601): some cores originate
// MESSAGE from an ephemeral port on the same P-CSCF.
return expected != nil && expected.Equal(remote.IP)
}
func (session *Session) dispatchPacket(packet sipPacket, respond func([]byte) error) {