fix(ims): align VoWiFi SIP profile behavior (#75)

This commit is contained in:
ihipop
2026-08-21 22:48:55 +08:00
committed by GitHub
parent 76af0784e1
commit bbccaceabc
12 changed files with 371 additions and 71 deletions
+20 -10
View File
@@ -42,8 +42,10 @@ type CarrierProfile struct {
IMSRegisterProfile string
IMSIPSecEncryption string
SMSCenter string
PANIEnabled *bool
PANICountry string
PANINode string
IMSUserAgent string
IMSDialURIScheme string
IMSUserEqPhone bool
IMSVoiceCodecs []string
@@ -57,7 +59,6 @@ type IMSRegisterOptions struct {
ContactExtraTags []string
SupportedHeader *string
AllowHeader *string
UserAgent string
PPreferredIdentity bool
PVisitedNetworkID string
PAccessNetworkInfo *string
@@ -68,6 +69,7 @@ type IMSRegisterOptions struct {
const (
IMSContactFormatStandard = "standard"
IMSContactFormatATT = "att"
IMSContactFormatGSMA = "gsma"
)
type carrierProfileDocument struct {
@@ -116,8 +118,10 @@ type carrierProfileIMS struct {
RegisterProfile string `json:"register_profile,omitempty"`
IPSecEncryption string `json:"ipsec_encryption,omitempty"`
SMSCenter string `json:"sms_center,omitempty"`
PANIEnabled *bool `json:"pani_enabled,omitempty"`
PANICountry string `json:"pani_country,omitempty"`
PANINode string `json:"pani_node,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
DialURIScheme string `json:"dial_uri_scheme,omitempty"`
UserEqPhone *bool `json:"user_eq_phone,omitempty"`
VoiceCodecs []string `json:"voice_codecs,omitempty"`
@@ -131,7 +135,6 @@ type carrierProfileRegisterOptions struct {
ContactExtraTags []string `json:"contact_extra_tags,omitempty"`
SupportedHeader *string `json:"supported_header,omitempty"`
AllowHeader *string `json:"allow_header,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
PPreferredIdentity bool `json:"p_preferred_identity,omitempty"`
PVisitedNetworkID string `json:"p_visited_network_id,omitempty"`
PAccessNetworkInfo *string `json:"p_access_network_info,omitempty"`
@@ -322,6 +325,7 @@ func validCarrierProfileRule(rule carrierProfileRule) bool {
return false
}
if country := strings.ToUpper(strings.TrimSpace(rule.IMS.PANICountry)); country != "" &&
country != "AUTO" &&
(len(country) != 2 || country[0] < 'A' || country[0] > 'Z' || country[1] < 'A' || country[1] > 'Z') {
return false
}
@@ -340,7 +344,7 @@ func validCarrierProfileRule(rule carrierProfileRule) bool {
return false
}
if format := strings.ToLower(strings.TrimSpace(rule.IMS.RegisterOptions.ContactFormat)); format != "" &&
format != IMSContactFormatStandard && format != IMSContactFormatATT {
format != IMSContactFormatStandard && format != IMSContactFormatATT && format != IMSContactFormatGSMA {
return false
}
for _, value := range rule.IMS.RegisterOptions.ContactExtraTags {
@@ -353,7 +357,7 @@ func validCarrierProfileRule(rule carrierProfileRule) bool {
return false
}
}
for _, value := range []string{rule.IMS.RegisterOptions.UserAgent, rule.IMS.RegisterOptions.PVisitedNetworkID, rule.IMS.RegisterOptions.CellularNetworkInfo} {
for _, value := range []string{rule.IMS.UserAgent, rule.IMS.RegisterOptions.PVisitedNetworkID, rule.IMS.RegisterOptions.CellularNetworkInfo} {
if strings.ContainsAny(value, "\r\n") {
return false
}
@@ -588,8 +592,15 @@ func applyCarrierProfileRule(base CarrierProfile, rule carrierProfileRule, sourc
base.IMSIPSecEncryption = value
}
base.SMSCenter = strings.TrimSpace(rule.IMS.SMSCenter)
if rule.IMS.PANIEnabled != nil {
enabled := *rule.IMS.PANIEnabled
base.PANIEnabled = &enabled
}
base.PANICountry = strings.ToUpper(strings.TrimSpace(rule.IMS.PANICountry))
base.PANINode = strings.TrimSpace(rule.IMS.PANINode)
if value := strings.TrimSpace(rule.IMS.UserAgent); value != "" {
base.IMSUserAgent = value
}
if value := strings.ToLower(strings.TrimSpace(rule.IMS.DialURIScheme)); value != "" {
base.IMSDialURIScheme = value
}
@@ -624,9 +635,6 @@ func applyRegisterOptions(base IMSRegisterOptions, rule carrierProfileRegisterOp
value := strings.TrimSpace(*rule.AllowHeader)
base.AllowHeader = &value
}
if value := strings.TrimSpace(rule.UserAgent); value != "" {
base.UserAgent = value
}
if rule.PPreferredIdentity {
base.PPreferredIdentity = true
}
@@ -723,8 +731,8 @@ func applyAssignedCarrierRoute(identity SIMIdentity) SIMIdentity {
if strings.TrimSpace(identity.ICCID) != "" {
if mcc, mnc, ok := HomePLMNFromICCID(identity.ICCID); ok {
imsiCountry := countryCodeForMCC(identity.HomeMCC)
iccidCountry := countryCodeForMCC(mcc)
imsiCountry := CountryCodeForMCC(identity.HomeMCC)
iccidCountry := CountryCodeForMCC(mcc)
if identity.HomeMCC == "" || (imsiCountry != "" && iccidCountry != "" && imsiCountry != iccidCountry) {
identity.HomeMCC = mcc
identity.HomeMNC = mnc
@@ -737,7 +745,9 @@ func applyAssignedCarrierRoute(identity SIMIdentity) SIMIdentity {
return identity
}
func countryCodeForMCC(mcc string) string {
// CountryCodeForMCC returns the ISO 3166-1 alpha-2 country code associated
// with an MCC known to the carrier compatibility database.
func CountryCodeForMCC(mcc string) string {
switch strings.TrimSpace(mcc) {
case "515":
return "PH"
+28
View File
@@ -46,6 +46,31 @@ func TestResolveCarrierProfileUsesAppleGID1Selector(t *testing.T) {
}
}
func TestResolveCarrierProfileGiffgaffIMSHeaders(t *testing.T) {
profile := ResolveCarrierProfile(SIMIdentity{
IMSI: "234100000000001", HomeMCC: "234", HomeMNC: "10", GID1: "508FFFFF",
})
options := profile.IMSRegisterOptions
if profile.IMSTransport != "tcp" || options.ContactFormat != IMSContactFormatGSMA {
t.Fatalf("giffgaff IMS transport/contact profile = %#v", profile)
}
if profile.IMSUserAgent != "iOS/18.6.2 iPhone" {
t.Fatalf("giffgaff User-Agent = %q", profile.IMSUserAgent)
}
if options.SupportedHeader != nil || options.AllowHeader != nil {
t.Fatalf("giffgaff REGISTER header overrides = supported=%v allow=%v", options.SupportedHeader, options.AllowHeader)
}
if options.PAccessNetworkInfo != nil {
t.Fatalf("giffgaff unexpectedly defines a carrier PANI override = %v", *options.PAccessNetworkInfo)
}
if profile.PANIEnabled == nil || !*profile.PANIEnabled || profile.PANICountry != "AUTO" {
t.Fatalf("giffgaff PANI behavior = enabled=%v country=%q", profile.PANIEnabled, profile.PANICountry)
}
if len(options.ContactExtraTags) != 2 || options.ContactExtraTags[0] != "+g.3gpp.mid-call" || options.ContactExtraTags[1] != "+g.3gpp.smsip" {
t.Fatalf("giffgaff Contact tags = %#v", options.ContactExtraTags)
}
}
func TestResolveCarrierProfileATT(t *testing.T) {
profile := ResolveCarrierProfile(SIMIdentity{
ICCID: "8901410000000000001", IMSI: "310410000000001", HomeMCC: "310", HomeMNC: "410",
@@ -79,6 +104,9 @@ func TestResolveCarrierProfileStandardHasNoRegisterOverrides(t *testing.T) {
if profile.IMSRegisterOptions.SupportedHeader != nil {
t.Fatalf("standard supported header = %v", *profile.IMSRegisterOptions.SupportedHeader)
}
if profile.PANIEnabled != nil || profile.PANICountry != "" {
t.Fatalf("standard PANI behavior = enabled=%v country=%q", profile.PANIEnabled, profile.PANICountry)
}
if profile.AllowSMSWithoutContactConfirmation {
t.Fatal("standard profile should require SMS contact confirmation")
}
+5 -4
View File
@@ -635,6 +635,11 @@ func importCarrierIMS(rule *carrierProfileRule, plists []ipccPlist, warnings *ip
warnings.add("disabled_ims_ipsec_ignored", "UseIPSec=false was not imported because VoWiFi IMS security cannot be weakened automatically", document.name+":"+strings.Join(signaling.path, ".")+".UseIPSec")
}
}
if strings.EqualFold(plistString(signaling.value["CountryOfOriginationFormat"]), "PANI") {
enabled := true
rule.IMS.PANIEnabled = &enabled
rule.IMS.PANICountry = "AUTO"
}
}
}
if useIPSec {
@@ -661,10 +666,6 @@ func inspectIgnoredCarrierFields(plists []ipccPlist, warnings *ipccWarningSet) {
warnings.add("apn_settings_ignored", "APN settings and credentials are outside the VoCat carrier-profile importer", fullPath)
case key == "media" && strings.Contains(strings.ToLower(strings.Join(keyPath, ".")), "imsconfig"):
warnings.add("device_media_overrides_ignored", "device-family media and codec overrides require hardware validation and were not imported", fullPath)
case key == "countryoforiginationformat":
// PANI is access/session metadata, not a carrier location constant.
// The IMS runtime provides one globally and consistently across
// REGISTER, MESSAGE, RP-ACK and dialogs, so no profile field is needed.
case strings.Contains(key, "emergency") || strings.Contains(key, "e911"):
warnings.add("emergency_settings_ignored", "emergency-service settings are never imported", fullPath)
}
+3
View File
@@ -76,6 +76,9 @@ func TestImportCarrierIPCCConvertsBinaryAndXMLPlistsSafely(t *testing.T) {
if rule.IMS.IPSecEncryption != "aes-cbc" {
t.Fatalf("converted IMS profile = %#v", rule.IMS)
}
if rule.IMS.PANIEnabled == nil || !*rule.IMS.PANIEnabled || rule.IMS.PANICountry != "AUTO" {
t.Fatalf("converted PANI behavior = enabled=%v country=%q", rule.IMS.PANIEnabled, rule.IMS.PANICountry)
}
for _, code := range []string{
"remote_certificate_bypass_ignored",
"disabled_dpd_ignored",
+12 -1
View File
@@ -5786,7 +5786,18 @@
"proposal": "modern"
},
"ims": {
"ipsec_encryption": "aes-cbc"
"transport": "tcp",
"ipsec_encryption": "aes-cbc",
"pani_enabled": true,
"pani_country": "AUTO",
"user_agent": "iOS/18.6.2 iPhone",
"register_options": {
"contact_format": "gsma",
"contact_extra_tags": [
"+g.3gpp.mid-call",
"+g.3gpp.smsip"
]
}
}
},
{
+3 -1
View File
@@ -393,9 +393,11 @@ func parseInnerIPv6(packet []byte) (innerPacketMetadata, error) {
return innerPacketMetadata{}, errors.New("ike: inner IPv6 packet is truncated")
}
payloadLength := int(binary.BigEndian.Uint16(packet[4:6]))
if payloadLength+40 != len(packet) {
declaredLength := payloadLength + 40
if declaredLength > len(packet) {
return innerPacketMetadata{}, errors.New("ike: inner IPv6 payload length is invalid")
}
packet = packet[:declaredLength]
metadata := innerPacketMetadata{
source: append(net.IP(nil), packet[8:24]...),
destination: append(net.IP(nil), packet[24:40]...),
+20
View File
@@ -318,6 +318,26 @@ func TestParseInnerIPv6ESP(t *testing.T) {
}
}
func TestParseInnerIPv6ESPTrimsTrailingAlignmentBytes(t *testing.T) {
t.Parallel()
packet := make([]byte, 40+20+4)
packet[0] = 0x60
binary.BigEndian.PutUint16(packet[4:6], 20)
packet[6] = 6
packet[7] = 64
copy(packet[8:24], net.ParseIP("2001:db8::1").To16())
copy(packet[24:40], net.ParseIP("2001:db8::2").To16())
binary.BigEndian.PutUint16(packet[40:42], 49686)
binary.BigEndian.PutUint16(packet[42:44], 5060)
metadata, err := parseInnerPacket(packet)
if err != nil {
t.Fatal(err)
}
if metadata.protocol != 6 || metadata.sourcePort != 49686 || metadata.destinationPort != 5060 {
t.Fatalf("metadata = %+v", metadata)
}
}
func mustDefaultESPTunnel(t *testing.T) *espTunnel {
t.Helper()
return mustTestESPTunnel(
+16 -15
View File
@@ -116,7 +116,7 @@ func (session *Session) DialCall(ctx context.Context, number string) (vowifi.Cal
"P-Preferred-Service: "+mmtelServiceURN,
`Accept-Contact: *;+g.3gpp.icsi-ref="`+mmtelFeatureTag+`"`,
"P-Access-Network-Info: "+session.pAccessNetworkInfo(),
"User-Agent: "+session.callUserAgent(),
"User-Agent: "+session.imsUserAgent(),
"Allow: INVITE, ACK, CANCEL, BYE, OPTIONS, MESSAGE, PRACK, UPDATE, INFO",
"Supported: 100rel, timer, replaces",
"Session-Expires: 1800;refresher=uac",
@@ -500,7 +500,7 @@ func (session *Session) sendRejectedInviteACK(call *imsCall, response *sipRespon
"Call-ID: "+call.callID,
fmt.Sprintf("CSeq: %d ACK", call.cseq),
"P-Access-Network-Info: "+session.pAccessNetworkInfo(),
"User-Agent: "+session.callUserAgent(),
"User-Agent: "+session.imsUserAgent(),
"Content-Length: 0", "", "",
)
session.writeMu.Lock()
@@ -578,7 +578,7 @@ func (session *Session) sendPRACK(call *imsCall, response *sipResponse) {
"From: "+from, "To: "+to, "Call-ID: "+call.callID,
fmt.Sprintf("CSeq: %d PRACK", cseq), "RAck: "+rseq+" "+inviteCSeq,
"P-Access-Network-Info: "+session.pAccessNetworkInfo(),
"User-Agent: "+session.callUserAgent(),
"User-Agent: "+session.imsUserAgent(),
"Content-Length: 0", "", "",
)
ctx, cancel := context.WithTimeout(session.refreshContext, 10*time.Second)
@@ -709,7 +709,7 @@ func (session *Session) buildDialogRequest(call *imsCall, method string, cseq ui
"Call-ID: "+call.callID,
fmt.Sprintf("CSeq: %d %s", cseq, method),
"Supported: 100rel, timer",
"User-Agent: "+session.callUserAgent(),
"User-Agent: "+session.imsUserAgent(),
)
if method != "CANCEL" {
lines = append(lines, "P-Access-Network-Info: "+session.pAccessNetworkInfo())
@@ -771,6 +771,13 @@ func (session *Session) dialogContactHeader() string {
if session == nil || session.conn == nil || strings.TrimSpace(session.identity.user) == "" {
return ""
}
if session.imsRegisterOptions().ContactFormat == vowifi.IMSContactFormatGSMA {
contact := "Contact: <sip:" + session.contactAddress() + `>;+g.3gpp.icsi-ref="` + mmtelFeatureTag + `"`
if strings.TrimSpace(session.instanceID) != "" {
contact += `;+sip.instance="<` + session.instanceID + `>"`
}
return contact
}
contact := "Contact: <sip:" + session.identity.user + "@" + session.contactAddress() + ";transport=" + session.transport + ">"
if strings.TrimSpace(session.instanceID) != "" {
contact += `;+sip.instance="<` + session.instanceID + `>"`
@@ -811,19 +818,13 @@ func (session *Session) callOriginatingIdentitiesLocked(profile vowifi.CarrierPr
}
func (session *Session) pAccessNetworkInfo() string {
if session == nil {
return ""
}
if session.paniResolved {
return ueProvidedPANI(session.pani)
return session.pani
}
return sessionPAccessNetworkInfo(session.instanceID)
}
func (session *Session) callUserAgent() string {
if session != nil && session.provider != nil {
if value := strings.TrimSpace(session.provider.config.UserAgent); value != "" {
return value
}
}
return "vocat/1"
return resolveSessionPAccessNetworkInfo(session.request.Identity, session.imsLogger())
}
func callResponseDiagnostic(response *sipResponse) string {
+1 -1
View File
@@ -229,7 +229,7 @@ func TestOutgoingLocalNumberUsesIMSPhoneContextAndMMTelHeaders(t *testing.T) {
"P-Preferred-Identity: <tel:+447700900123>\r\n",
"P-Preferred-Service: " + mmtelServiceURN + "\r\n",
`Accept-Contact: *;+g.3gpp.icsi-ref="` + mmtelFeatureTag + `"` + "\r\n",
"P-Access-Network-Info: " + sessionPAccessNetworkInfo(session.instanceID) + "\r\n",
"P-Access-Network-Info: IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode + "\r\n",
"User-Agent: VoCat Test\r\n",
"Accept: application/sdp\r\n",
} {
+128 -26
View File
@@ -4,7 +4,6 @@ import (
"bufio"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
@@ -24,6 +23,7 @@ const (
defaultRegistrationExpiry = 3600 * time.Second
defaultTransactionTimeout = 12 * time.Second
maxAuthenticationChallenges = 3
defaultPANIWLANNode = "ffffffffffff"
)
var (
@@ -649,6 +649,11 @@ func newSession(
if err != nil {
return nil, err
}
instanceURI := "urn:uuid:" + instanceID
profile := vowifi.ResolveCarrierProfile(request.Identity)
if profile.IMSRegisterOptions.ContactFormat == vowifi.IMSContactFormatGSMA {
instanceURI = sipInstanceID(request.Identity, instanceID)
}
refreshContext, refreshCancel := context.WithCancel(context.Background())
session := &Session{
provider: provider,
@@ -660,8 +665,8 @@ func newSession(
conn: connection,
callID: callToken + "@" + addressHost(connection.LocalAddr()),
fromTag: fromTag,
instanceID: "urn:uuid:" + instanceID,
pani: resolveSessionPAccessNetworkInfo(request.Identity, "urn:uuid:"+instanceID),
instanceID: instanceURI,
pani: resolveSessionPAccessNetworkInfo(request.Identity, provider.config.Logger),
paniResolved: true,
cseq: 1,
refreshContext: refreshContext,
@@ -970,11 +975,7 @@ func (session *Session) buildRegister(
allow = *registerOptions.AllowHeader
}
userAgent := strings.TrimSpace(session.provider.config.UserAgent)
if override := strings.TrimSpace(registerOptions.UserAgent); override != "" &&
(userAgent == "" || userAgent == "vocat/1") {
userAgent = override
}
userAgent := session.imsUserAgent()
lines := []string{
"REGISTER " + requestURI + " SIP/2.0",
@@ -1064,6 +1065,15 @@ func (session *Session) buildContact(contactAddress string, registerOptions vowi
`%s%s;audio;+g.3gpp.smsip;+g.3gpp.icsi-ref="%s";+sip.instance="<%s>"`,
base, extra, icsiRef, instanceID,
)
case vowifi.IMSContactFormatGSMA:
extra := ""
for _, tag := range registerOptions.ContactExtraTags {
extra += ";" + tag
}
return fmt.Sprintf(
`<sip:%s>;+g.3gpp.icsi-ref="%s"%s;+sip.instance="<%s>"`,
contactAddress, icsiRef, extra, instanceID,
)
default:
extra := ""
for _, tag := range registerOptions.ContactExtraTags {
@@ -1076,41 +1086,127 @@ func (session *Session) buildContact(contactAddress string, registerOptions vowi
}
}
// sessionPAccessNetworkInfo creates a syntactically valid, locally
// administered unicast WLAN node identifier from the already-random SIP
// instance ID. It discloses neither a real BSSID nor subscriber identity, but
// remains stable for every transaction belonging to this IMS registration.
func sessionPAccessNetworkInfo(instanceID string) string {
instanceID = strings.TrimSpace(instanceID)
if instanceID == "" {
return ""
// sipInstanceID uses the standardized GSMA device-instance URI when a valid
// modem identity is available and keeps the generated UUID as the fallback.
func sipInstanceID(identity vowifi.SIMIdentity, fallback string) string {
imei := strings.TrimSpace(identity.IMEI)
if len(imei) == 15 {
valid := true
for _, digit := range imei {
if digit < '0' || digit > '9' {
valid = false
break
}
}
if valid {
return "urn:gsma:imei:" + imei + "-0"
}
}
digest := sha256.Sum256([]byte(instanceID))
digest[0] = (digest[0] | 0x02) & 0xfe // locally administered, unicast
return "IEEE-802.11;i-wlan-node-id=" + hex.EncodeToString(digest[:6])
return "urn:uuid:" + strings.TrimSpace(fallback)
}
func (session *Session) imsRegisterOptions() vowifi.IMSRegisterOptions {
if session == nil {
return vowifi.IMSRegisterOptions{}
}
return vowifi.ResolveCarrierProfile(session.request.Identity).IMSRegisterOptions
}
func (session *Session) imsUserAgent() string {
if session != nil && session.provider != nil {
if value := strings.TrimSpace(session.provider.config.UserAgent); value != "" {
if value != "vocat/1" {
return value
}
}
}
if session != nil {
profile := vowifi.ResolveCarrierProfile(session.request.Identity)
if value := strings.TrimSpace(profile.IMSUserAgent); value != "" {
return value
}
}
if session != nil && session.provider != nil {
if value := strings.TrimSpace(session.provider.config.UserAgent); value != "" {
return value
}
}
return "vocat/1"
}
func (session *Session) imsLogger() *slog.Logger {
if session != nil && session.provider != nil && session.provider.config.Logger != nil {
return session.provider.config.Logger
}
return slog.Default()
}
// resolveSessionPAccessNetworkInfo freezes the selected value when the IMS
// session is created. This prevents a carrier-profile reload from changing
// access identity between REGISTER, SMS MESSAGE and its RP-ACK.
func resolveSessionPAccessNetworkInfo(identity vowifi.SIMIdentity, instanceID string) string {
func resolveSessionPAccessNetworkInfo(identity vowifi.SIMIdentity, logger *slog.Logger) string {
if logger == nil {
logger = slog.Default()
}
profile := vowifi.ResolveCarrierProfile(identity)
if profile.PANIEnabled != nil && !*profile.PANIEnabled {
return ""
}
if configured := profile.IMSRegisterOptions.PAccessNetworkInfo; configured != nil {
return ueProvidedPANI(*configured)
return appendPaniCountry(ueProvidedPANI(*configured), identity, profile, logger)
}
node := strings.ToLower(strings.TrimSpace(profile.PANINode))
if decoded, err := hex.DecodeString(node); err != nil || len(decoded) != 6 {
node = strings.TrimPrefix(sessionPAccessNetworkInfo(instanceID), "IEEE-802.11;i-wlan-node-id=")
node = defaultPANIWLANNode
}
if node == "" {
return ""
}
value := "IEEE-802.11;i-wlan-node-id=" + node
if country := strings.ToUpper(strings.TrimSpace(profile.PANICountry)); country != "" {
value += ";country=" + country
return appendPaniCountry(value, identity, profile, logger)
}
func appendPaniCountry(value string, identity vowifi.SIMIdentity, profile vowifi.CarrierProfile, logger *slog.Logger) string {
value = strings.TrimSpace(value)
if value == "" {
return value
}
return value
parts := strings.Split(value, ";")
for _, parameter := range parts {
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(parameter)), "country=") {
return value
}
}
countryMode := strings.ToUpper(strings.TrimSpace(profile.PANICountry))
country := countryMode
if countryMode == "AUTO" {
mcc := strings.TrimSpace(identity.HomeMCC)
if mcc == "" {
mcc = strings.TrimSpace(profile.RouteMCC)
}
country = vowifi.CountryCodeForMCC(mcc)
if country == "" {
if logger == nil {
logger = slog.Default()
}
logger.Error("IMS PANI country code could not be derived",
"category", "ims",
"stage", "pani_country",
"carrier_profile", profile.ID,
"mcc", mcc,
)
return value
}
}
if country == "" {
return value
}
parts = append(parts, "")
copy(parts[2:], parts[1:])
parts[1] = "country=" + country
return strings.Join(parts, ";")
}
// ueProvidedPANI removes the network-provided marker from a profile override.
@@ -1263,6 +1359,7 @@ func (session *Session) applyRegistrationEvidence(response *sipResponse) error {
serviceRoutes := splitHeaderValues(response.values("Service-Route"))
registeredContact := ""
smsConfirmed := false
profile := vowifi.ResolveCarrierProfile(session.request.Identity)
instanceLower := strings.ToLower(session.instanceID)
contactURILower := strings.ToLower(fmt.Sprintf(
"sip:%s@%s;transport=%s",
@@ -1270,10 +1367,15 @@ func (session *Session) applyRegistrationEvidence(response *sipResponse) error {
session.contactAddress(),
session.transport,
))
contactAddressLower := ""
if profile.IMSRegisterOptions.ContactFormat == vowifi.IMSContactFormatGSMA {
contactAddressLower = strings.ToLower("sip:" + session.contactAddress())
}
for _, contact := range contacts {
lower := strings.ToLower(contact)
matchesThisSession := strings.Contains(lower, instanceLower) ||
strings.Contains(lower, contactURILower)
strings.Contains(lower, contactURILower) ||
(contactAddressLower != "" && strings.Contains(lower, contactAddressLower))
if matchesThisSession {
registeredContact = contact
smsConfirmed = strings.Contains(lower, "+g.3gpp.smsip")
+134 -13
View File
@@ -506,13 +506,8 @@ func serveRegistration(listener *net.UDPConn, nonce string, confirmSMS bool) err
}
func TestSessionPAccessNetworkInfoIsStableAndUEProvided(t *testing.T) {
instanceID := "urn:uuid:00000000-0000-4000-8000-000000000001"
first := sessionPAccessNetworkInfo(instanceID)
second := sessionPAccessNetworkInfo(instanceID)
if first != second {
t.Fatalf("PANI changed for one SIP instance: %q != %q", first, second)
}
if err := validateTestPANI(first); err != nil {
defaultPANI := "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode
if err := validateTestPANI(defaultPANI); err != nil {
t.Fatal(err)
}
if got := ueProvidedPANI(" IEEE-802.11;i-wlan-node-id=aabbccddeeff;network-provided "); got != "IEEE-802.11;i-wlan-node-id=aabbccddeeff" {
@@ -521,23 +516,149 @@ func TestSessionPAccessNetworkInfoIsStableAndUEProvided(t *testing.T) {
if got := ueProvidedPANI("network-provided"); got != "" {
t.Fatalf("marker-only PANI = %q, want empty", got)
}
if got := (&Session{paniResolved: true}).pAccessNetworkInfo(); got != "" {
t.Fatalf("explicitly omitted session PANI = %q, want empty", got)
if got := (&Session{pani: "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode, paniResolved: true}).pAccessNetworkInfo(); got != "IEEE-802.11;i-wlan-node-id="+defaultPANIWLANNode {
t.Fatalf("session PANI = %q, want default WLAN node", got)
}
}
func TestPAccessNetworkInfoUsesDefaultNodeAndConditionalCountry(t *testing.T) {
cases := []struct {
name string
identity vowifi.SIMIdentity
want string
}{
{
name: "standard without PANI country format",
identity: vowifi.SIMIdentity{IMSI: "001010123456789", HomeMCC: "001", HomeMNC: "01"},
want: "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode,
},
{
name: "giffgaff with IPCC PANI country format",
identity: vowifi.SIMIdentity{IMSI: "234100000000001", HomeMCC: "234", HomeMNC: "10", GID1: "508FFFFF"},
want: "IEEE-802.11;country=GB;i-wlan-node-id=" + defaultPANIWLANNode,
},
{
name: "AT&T without PANI country format",
identity: vowifi.SIMIdentity{IMSI: "310410000000001", HomeMCC: "310", HomeMNC: "410"},
want: "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode,
},
{
name: "VOXI without PANI country format",
identity: vowifi.SIMIdentity{IMSI: "234150000000001", HomeMCC: "234", HomeMNC: "15", SPN: "VOXI"},
want: "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode,
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
got := resolveSessionPAccessNetworkInfo(test.identity, slog.New(slog.NewTextHandler(io.Discard, nil)))
if got != test.want {
t.Fatalf("PANI = %q, want %q", got, test.want)
}
})
}
}
func TestAppendPaniCountryModes(t *testing.T) {
base := "IEEE-802.11;i-wlan-node-id=" + defaultPANIWLANNode
identity := vowifi.SIMIdentity{HomeMCC: "234"}
if got := appendPaniCountry(base, identity, vowifi.CarrierProfile{}, slog.Default()); got != base {
t.Fatalf("empty PANI country = %q, want %q", got, base)
}
if got := appendPaniCountry(base, identity, vowifi.CarrierProfile{PANICountry: "GB"}, slog.Default()); got != "IEEE-802.11;country=GB;i-wlan-node-id="+defaultPANIWLANNode {
t.Fatalf("fixed PANI country = %q", got)
}
if got := appendPaniCountry(base, identity, vowifi.CarrierProfile{PANICountry: "AUTO"}, slog.Default()); got != "IEEE-802.11;country=GB;i-wlan-node-id="+defaultPANIWLANNode {
t.Fatalf("automatic PANI country = %q", got)
}
var logs strings.Builder
logger := slog.New(slog.NewTextHandler(&logs, nil))
got := appendPaniCountry(base, vowifi.SIMIdentity{}, vowifi.CarrierProfile{ID: "test-auto", PANICountry: "AUTO"}, logger)
if got != base || !strings.Contains(logs.String(), "IMS PANI country code could not be derived") {
t.Fatalf("failed automatic PANI country = %q, logs = %q", got, logs.String())
}
}
func TestIMSProfileUserAgentUsesUnifiedHeaderValue(t *testing.T) {
giffgaff := &Session{request: vowifi.IMSRequest{Identity: vowifi.SIMIdentity{
IMSI: "234100000000001", HomeMCC: "234", HomeMNC: "10", GID1: "508FFFFF",
}}}
if got := giffgaff.imsUserAgent(); got != "iOS/18.6.2 iPhone" {
t.Fatalf("giffgaff IMS User-Agent = %q", got)
}
if options := giffgaff.imsRegisterOptions(); options.AllowHeader != nil || options.SupportedHeader != nil {
t.Fatalf("giffgaff REGISTER capability overrides leaked from business headers: %#v", options)
}
standard := &Session{request: vowifi.IMSRequest{Identity: vowifi.SIMIdentity{
IMSI: "999010000000001", HomeMCC: "999", HomeMNC: "01",
}}}
if got := standard.imsUserAgent(); got != "vocat/1" {
t.Fatalf("standard IMS User-Agent fallback = %q", got)
}
}
func TestSipInstanceIDUsesGSMAFormWhenIMEIIsAvailable(t *testing.T) {
identity := vowifi.SIMIdentity{IMEI: "353024112557010"}
if got := sipInstanceID(identity, "00000000-0000-4000-8000-000000000001"); got != "urn:gsma:imei:353024112557010-0" {
t.Fatalf("sipInstanceID() = %q", got)
}
if got := sipInstanceID(vowifi.SIMIdentity{IMEI: "not-an-imei"}, "00000000-0000-4000-8000-000000000001"); got != "urn:uuid:00000000-0000-4000-8000-000000000001" {
t.Fatalf("sipInstanceID() fallback = %q", got)
}
}
func TestGSMAContactFormatUsesAddressAndDeviceInstance(t *testing.T) {
session := &Session{
identity: identitySet{user: "234105776448519"},
transport: "tcp",
instanceID: "urn:gsma:imei:353024112557010-0",
}
got := session.buildContact("[2001:db8::1]:49686", vowifi.IMSRegisterOptions{
ContactFormat: vowifi.IMSContactFormatGSMA,
ContactExtraTags: []string{"+g.3gpp.mid-call", "+g.3gpp.smsip"},
})
want := `<sip:[2001:db8::1]:49686>;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel";+g.3gpp.mid-call;+g.3gpp.smsip;+sip.instance="<urn:gsma:imei:353024112557010-0>"`
if got != want {
t.Fatalf("GSMA Contact = %q, want %q", got, want)
}
}
func validateTestPANI(value string) error {
const prefix = "IEEE-802.11;i-wlan-node-id="
if !strings.HasPrefix(value, prefix) {
return fmt.Errorf("value %q does not start with %q", value, prefix)
const accessType = "IEEE-802.11"
if !strings.HasPrefix(value, accessType+";") {
return fmt.Errorf("value %q does not start with %q", value, accessType+";")
}
if strings.Contains(strings.ToLower(value), "network-provided") {
return fmt.Errorf("UE PANI incorrectly claims network-provided provenance: %q", value)
}
node, err := hex.DecodeString(strings.TrimPrefix(value, prefix))
var nodeValue, country string
for _, parameter := range strings.Split(strings.TrimPrefix(value, accessType+";"), ";") {
key, parameterValue, ok := strings.Cut(parameter, "=")
if !ok {
continue
}
switch strings.ToLower(strings.TrimSpace(key)) {
case "i-wlan-node-id":
nodeValue = strings.TrimSpace(parameterValue)
case "country":
country = strings.TrimSpace(parameterValue)
}
}
if nodeValue == "" {
return fmt.Errorf("i-wlan-node-id is missing: %q", value)
}
node, err := hex.DecodeString(nodeValue)
if err != nil || len(node) != 6 {
return fmt.Errorf("i-wlan-node-id must be 12 hexadecimal digits: %q", value)
}
if strings.EqualFold(nodeValue, defaultPANIWLANNode) {
if country != "" && len(country) != 2 {
return fmt.Errorf("country must be an ISO alpha-2 code: %q", value)
}
return nil
}
if node[0]&0x03 != 0x02 {
return fmt.Errorf("i-wlan-node-id must be a locally administered unicast identifier: %q", value)
}
+1
View File
@@ -1177,6 +1177,7 @@ func (session *Session) sendSIPMessageWith(
lines = append(lines,
"Request-Disposition: no-fork",
"Allow: MESSAGE",
"User-Agent: "+session.imsUserAgent(),
)
if inReplyTo != "" {
lines = append(lines, "In-Reply-To: "+inReplyTo)