mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-21 23:33:42 +08:00
fix(ims): align VoWiFi SIP profile behavior (#75)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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]...),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user