diff --git a/internal/vowifi/ims/provider.go b/internal/vowifi/ims/provider.go index bf4f243..87ce2eb 100644 --- a/internal/vowifi/ims/provider.go +++ b/internal/vowifi/ims/provider.go @@ -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, diff --git a/internal/vowifi/ims/security.go b/internal/vowifi/ims/security.go index ab1e91e..5765585 100644 --- a/internal/vowifi/ims/security.go +++ b/internal/vowifi/ims/security.go @@ -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 { diff --git a/internal/vowifi/ims/sms_runtime.go b/internal/vowifi/ims/sms_runtime.go index 9320fed..de4f03e 100644 --- a/internal/vowifi/ims/sms_runtime.go +++ b/internal/vowifi/ims/sms_runtime.go @@ -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) {