mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
feat: implement carrier profile resolution system and initial profile database for VoWiFi connectivity
This commit is contained in:
@@ -330,6 +330,18 @@ func (manager *Manager) openEuiccAID(ctx context.Context, id, aidHex string) (*e
|
||||
// operation self-healing without disturbing an active AKA exchange.
|
||||
continue
|
||||
}
|
||||
if attempt == 1 && isTransientEuiccCME(err) {
|
||||
// When SIM hot-swap occurs or the modem baseband APDU channel is stuck (+CME ERROR: 0),
|
||||
// perform a soft SIM subsystem reset (AT+CFUN=0 -> AT+CFUN=1/4) to re-initialize
|
||||
// card interface voltage and ATR without restarting the whole hardware module.
|
||||
_ = manager.softResetForProfileSwitch(ctx, id)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(600 * time.Millisecond):
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !isTransientEuiccCME(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -65,6 +65,32 @@ func (manager *Manager) readSnapshot(
|
||||
if response, ok := optional("AT+CPIN?"); ok {
|
||||
snapshot.SIMStatus, snapshot.SIMReady = parseCPIN(response)
|
||||
}
|
||||
previousICCID = strings.TrimSpace(previousICCID)
|
||||
if !snapshot.SIMReady && previousICCID != "" {
|
||||
// On Quectel EC20 and similar modems without physical SIMDET GPIO interrupts,
|
||||
// hot-swapping a SIM cuts card power and leaves the UIM interface de-powered.
|
||||
// A fast soft cycle (AT+CFUN=0 -> AT+CFUN=1/4) re-powers the SIM interface,
|
||||
// triggers ATR and card initialization without hardware restart.
|
||||
_, _ = manager.command(ctx, client, "AT+CFUN=0")
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return snapshot, ctx.Err()
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
targetCFUN := "AT+CFUN=1"
|
||||
if snapshot.FlightMode {
|
||||
targetCFUN = "AT+CFUN=4"
|
||||
}
|
||||
_, _ = manager.command(ctx, client, targetCFUN)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return snapshot, ctx.Err()
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
if response, ok := optional("AT+CPIN?"); ok {
|
||||
snapshot.SIMStatus, snapshot.SIMReady = parseCPIN(response)
|
||||
}
|
||||
}
|
||||
ccid, ccidErr := manager.command(ctx, client, "AT+CCID")
|
||||
if ccidErr != nil {
|
||||
ccid, ccidErr = manager.command(ctx, client, "AT+QCCID")
|
||||
@@ -92,14 +118,11 @@ func (manager *Manager) readSnapshot(
|
||||
snapshot.ICCID = parseICCIDIdentifier(ccid, []string{"+CCID:", "+QCCID:"}, 18, 22)
|
||||
}
|
||||
}
|
||||
previousICCID = strings.TrimSpace(previousICCID)
|
||||
if previousICCID != "" && snapshot.ICCID != "" && !strings.EqualFold(previousICCID, snapshot.ICCID) {
|
||||
// A different physical SIM must never inherit the previous card's
|
||||
// permission to use cellular RF. Disable RF before reading serving-cell
|
||||
// or operator state; policy reconciliation will then start VoWiFi.
|
||||
if _, err := manager.command(ctx, client, "AT+CFUN=4"); err != nil {
|
||||
return snapshot, fmt.Errorf("protect changed SIM with RF off: %w", err)
|
||||
}
|
||||
_, _ = manager.command(ctx, client, "AT+CFUN=4")
|
||||
snapshot.SIMChanged = true
|
||||
}
|
||||
if response, ok := optional("AT+CIMI"); ok {
|
||||
|
||||
@@ -396,8 +396,8 @@ func decimalString(value string) bool {
|
||||
// ResolveCarrierProfile returns the most specific built-in match. Exact SIM
|
||||
// attributes add specificity, so a constrained MVNO rule wins over its host
|
||||
// PLMN without weakening the default match for unrelated subscriptions.
|
||||
func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile {
|
||||
resolved := CarrierProfile{
|
||||
func defaultCarrierProfile() CarrierProfile {
|
||||
return CarrierProfile{
|
||||
ID: CarrierProfileStandard,
|
||||
MatchSource: "standard",
|
||||
IKEProposal: IKEProposalModern,
|
||||
@@ -408,6 +408,13 @@ func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile {
|
||||
IMSDialURIScheme: "tel",
|
||||
IMSVoiceCodecs: []string{"PCMA", "PCMU"},
|
||||
}
|
||||
}
|
||||
|
||||
// ResolveCarrierProfile returns the most specific built-in match. Exact SIM
|
||||
// attributes add specificity, so a constrained MVNO rule wins over its host
|
||||
// PLMN without weakening the default match for unrelated subscriptions.
|
||||
func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile {
|
||||
resolved := defaultCarrierProfile()
|
||||
bestScore := -1
|
||||
for _, rule := range carrierProfilesSnapshot() {
|
||||
score, source, matched := matchCarrierProfileRule(rule, identity)
|
||||
@@ -415,7 +422,7 @@ func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile {
|
||||
continue
|
||||
}
|
||||
bestScore = score
|
||||
resolved = applyCarrierProfileRule(resolved, rule, source, identity)
|
||||
resolved = applyCarrierProfileRule(defaultCarrierProfile(), rule, source, identity)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
@@ -458,6 +465,8 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int,
|
||||
score += 100
|
||||
sources = append(sources, "hplmn")
|
||||
hasHomePLMNMatch = true
|
||||
} else if identity.HomeMCC != "" && identity.HomeMNC != "" {
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
hasSelectorMatch := false
|
||||
@@ -487,7 +496,7 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int,
|
||||
score += selector.weight
|
||||
sources = append(sources, selector.name)
|
||||
hasSelectorMatch = true
|
||||
} else if !hasHomePLMNMatch {
|
||||
} else if !hasHomePLMNMatch || selector.name == "gid1" || selector.name == "gid2" {
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
@@ -499,6 +508,8 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int,
|
||||
score += 20
|
||||
sources = append(sources, "spn")
|
||||
hasSelectorMatch = true
|
||||
} else if !hasHomePLMNMatch || spn != "" {
|
||||
return 0, "", false
|
||||
}
|
||||
}
|
||||
if !hasHomePLMNMatch && !hasSelectorMatch {
|
||||
|
||||
@@ -73,3 +73,47 @@ func TestResolveCarrierProfileStandardHasNoRegisterOverrides(t *testing.T) {
|
||||
t.Fatal("standard profile should require SMS contact confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMVNOParentNetworkRouting(t *testing.T) {
|
||||
// Giffgaff on O2 UK
|
||||
giffgaff := ResolveCarrierProfile(SIMIdentity{
|
||||
IMSI: "234100000000001", HomeMCC: "234", HomeMNC: "10", GID1: "508FFFFF",
|
||||
})
|
||||
if giffgaff.RouteMCC != "234" || giffgaff.RouteMNC != "10" {
|
||||
t.Fatalf("giffgaff Route PLMN = %s-%s, want 234-10", giffgaff.RouteMCC, giffgaff.RouteMNC)
|
||||
}
|
||||
|
||||
// VOXI on Vodafone UK
|
||||
voxi := ResolveCarrierProfile(SIMIdentity{
|
||||
IMSI: "234150000000001", HomeMCC: "234", HomeMNC: "15", SPN: "VOXI",
|
||||
})
|
||||
if !strings.Contains(voxi.ID, "voxi") || voxi.RouteMCC != "234" || voxi.RouteMNC != "15" {
|
||||
t.Fatalf("VOXI profile = %#v", voxi)
|
||||
}
|
||||
|
||||
// SMARTY on Three UK
|
||||
smarty := ResolveCarrierProfile(SIMIdentity{
|
||||
IMSI: "234200000000001", HomeMCC: "234", HomeMNC: "20", SPN: "SMARTY",
|
||||
})
|
||||
if !strings.Contains(smarty.ID, "smarty") || smarty.RouteMCC != "234" || smarty.RouteMNC != "20" {
|
||||
t.Fatalf("SMARTY profile = %#v", smarty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalRoamingProviderResolution(t *testing.T) {
|
||||
// Truphone / BetterRoaming global 90143
|
||||
truphone := ResolveCarrierProfile(SIMIdentity{
|
||||
IMSI: "901430000000001", HomeMCC: "901", HomeMNC: "43",
|
||||
})
|
||||
if (!strings.Contains(truphone.ID, "truphone") && !strings.Contains(truphone.ID, "1global")) || truphone.EPDG != "epdg.eps.truphone.net" {
|
||||
t.Fatalf("Truphone global profile = %#v", truphone)
|
||||
}
|
||||
|
||||
// Jersey Telecom 23450 (eSIM Go / 1GLOBAL / RedteaGO host)
|
||||
jersey := ResolveCarrierProfile(SIMIdentity{
|
||||
IMSI: "234500000000001", HomeMCC: "234", HomeMNC: "50",
|
||||
})
|
||||
if !strings.Contains(jersey.ID, "jersey-telecom") || jersey.EPDG != "epdg.epc.mnc050.mcc234.pub.3gppnetwork.org" {
|
||||
t.Fatalf("Jersey Telecom profile = %#v", jersey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4054,6 +4054,19 @@
|
||||
"home_plmns": [
|
||||
"23450"
|
||||
]
|
||||
},
|
||||
"route": {
|
||||
"mcc": "234",
|
||||
"mnc": "50"
|
||||
},
|
||||
"epdg": {
|
||||
"hostname": "epdg.epc.mnc050.mcc234.pub.3gppnetwork.org"
|
||||
},
|
||||
"ike": {
|
||||
"proposal": "modern"
|
||||
},
|
||||
"ims": {
|
||||
"ipsec_encryption": "aes-cbc"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -5724,13 +5737,27 @@
|
||||
},
|
||||
{
|
||||
"id": "ipcc-giffgaff-23410",
|
||||
"match": {
|
||||
"home_plmns": [
|
||||
"23410"
|
||||
],
|
||||
"gid1_prefixes": [
|
||||
"508"
|
||||
]
|
||||
"match_any": [
|
||||
{
|
||||
"home_plmns": [
|
||||
"23410"
|
||||
],
|
||||
"gid1_prefixes": [
|
||||
"508"
|
||||
]
|
||||
},
|
||||
{
|
||||
"home_plmns": [
|
||||
"23410"
|
||||
],
|
||||
"spns": [
|
||||
"giffgaff"
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"mcc": "234",
|
||||
"mnc": "10"
|
||||
},
|
||||
"epdg": {
|
||||
"hostname": "epdg.epc.mnc010.mcc234.pub.3gppnetwork.org"
|
||||
@@ -5742,6 +5769,74 @@
|
||||
"ipsec_encryption": "aes-cbc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ipcc-voxi-23415",
|
||||
"match_any": [
|
||||
{
|
||||
"home_plmns": [
|
||||
"23415"
|
||||
],
|
||||
"spns": [
|
||||
"VOXI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"home_plmns": [
|
||||
"23415"
|
||||
],
|
||||
"gid1_prefixes": [
|
||||
"4E"
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"mcc": "234",
|
||||
"mnc": "15"
|
||||
},
|
||||
"epdg": {
|
||||
"hostname": "epdg.epc.mnc015.mcc234.pub.3gppnetwork.org"
|
||||
},
|
||||
"ike": {
|
||||
"proposal": "modern"
|
||||
},
|
||||
"ims": {
|
||||
"ipsec_encryption": "aes-cbc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ipcc-smarty-23420",
|
||||
"match_any": [
|
||||
{
|
||||
"home_plmns": [
|
||||
"23420"
|
||||
],
|
||||
"spns": [
|
||||
"SMARTY"
|
||||
]
|
||||
},
|
||||
{
|
||||
"home_plmns": [
|
||||
"23420"
|
||||
],
|
||||
"gid1_prefixes": [
|
||||
"534D41525459"
|
||||
]
|
||||
}
|
||||
],
|
||||
"route": {
|
||||
"mcc": "234",
|
||||
"mnc": "20"
|
||||
},
|
||||
"epdg": {
|
||||
"hostname": "epdg.epc.mnc020.mcc234.pub.3gppnetwork.org"
|
||||
},
|
||||
"ike": {
|
||||
"proposal": "modern"
|
||||
},
|
||||
"ims": {
|
||||
"ipsec_encryption": "aes-cbc"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ipcc-o2-23410",
|
||||
"match_any": [
|
||||
@@ -9660,6 +9755,19 @@
|
||||
"gid1_prefixes": [
|
||||
"547275554B3030656E"
|
||||
]
|
||||
},
|
||||
{
|
||||
"home_plmns": [
|
||||
"90143",
|
||||
"90128"
|
||||
]
|
||||
},
|
||||
{
|
||||
"spns": [
|
||||
"Truphone",
|
||||
"BetterRoaming",
|
||||
"1GLOBAL"
|
||||
]
|
||||
}
|
||||
],
|
||||
"epdg": {
|
||||
|
||||
@@ -264,8 +264,13 @@ func permanentAKAIdentity(identity vowifi.SIMIdentity) ([]byte, error) {
|
||||
return nil, errors.New("ike: IMSI contains a non-digit")
|
||||
}
|
||||
}
|
||||
mcc := strings.TrimSpace(identity.HomeMCC)
|
||||
mnc := strings.TrimSpace(identity.HomeMNC)
|
||||
profile := vowifi.ResolveCarrierProfile(identity)
|
||||
mcc := strings.TrimSpace(profile.RouteMCC)
|
||||
mnc := strings.TrimSpace(profile.RouteMNC)
|
||||
if mcc == "" || mnc == "" {
|
||||
mcc = strings.TrimSpace(identity.HomeMCC)
|
||||
mnc = strings.TrimSpace(identity.HomeMNC)
|
||||
}
|
||||
if len(mcc) != 3 || (len(mnc) != 2 && len(mnc) != 3) {
|
||||
return nil, errors.New("ike: explicit home MCC/MNC is required for EAP-AKA")
|
||||
}
|
||||
|
||||
@@ -29,21 +29,28 @@ func resolveEPDG(ctx context.Context, resolver *net.Resolver, host string) ([]ne
|
||||
resolver = net.DefaultResolver
|
||||
}
|
||||
normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), "."))
|
||||
addresses, systemErr := resolver.LookupIPAddr(ctx, host)
|
||||
validSystemAddresses := filterValidPublicEPDGAddresses(addresses)
|
||||
if systemErr == nil && len(validSystemAddresses) > 0 {
|
||||
return validSystemAddresses, nil
|
||||
hostsToTry := []string{normalized}
|
||||
if alt := alternate3GPPHostname(normalized); alt != "" && alt != normalized {
|
||||
hostsToTry = append(hostsToTry, alt)
|
||||
}
|
||||
|
||||
var systemErr error
|
||||
for _, targetHost := range hostsToTry {
|
||||
addresses, err := resolver.LookupIPAddr(ctx, targetHost)
|
||||
if err == nil {
|
||||
valid := filterValidPublicEPDGAddresses(addresses)
|
||||
if len(valid) > 0 {
|
||||
return valid, nil
|
||||
}
|
||||
} else {
|
||||
systemErr = err
|
||||
}
|
||||
}
|
||||
|
||||
subnet := vowifi.EPDGDNSClientSubnet(normalized)
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
var fallbackErr error
|
||||
|
||||
hostsToTry := []string{normalized}
|
||||
if alt := alternate3GPPHostname(normalized); alt != "" && alt != normalized {
|
||||
hostsToTry = append(hostsToTry, alt)
|
||||
}
|
||||
|
||||
for _, targetHost := range hostsToTry {
|
||||
var fallback []net.IPAddr
|
||||
fallback, fallbackErr = resolveEPDGWithECS(ctx, client, googleDNSOverHTTPS, targetHost, subnet)
|
||||
|
||||
@@ -230,99 +230,109 @@ func (provider *Provider) Start(ctx context.Context, request vowifi.IMSRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pcscf := provider.config.PCSCF
|
||||
if pcscf == "" {
|
||||
var pcscfCandidates []string
|
||||
if provider.config.PCSCF != "" {
|
||||
pcscfCandidates = []string{provider.config.PCSCF}
|
||||
} else {
|
||||
for _, candidate := range tunnel.PCSCF {
|
||||
if strings.TrimSpace(candidate) != "" {
|
||||
pcscf = candidate
|
||||
break
|
||||
candidate = strings.TrimSpace(candidate)
|
||||
if candidate != "" {
|
||||
pcscfCandidates = append(pcscfCandidates, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
if pcscf == "" {
|
||||
if len(pcscfCandidates) == 0 {
|
||||
return nil, errors.New("ims: tunnel did not provide a P-CSCF")
|
||||
}
|
||||
endpoint, transportHint, err := parsePCSCF(pcscf, provider.config.Port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) {
|
||||
return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel")
|
||||
}
|
||||
transport, carrierSelected := carrierTransportForIdentity(provider.config, request.Identity)
|
||||
if cached := provider.cachedTransport(request.Identity); cached != "" {
|
||||
transport = cached
|
||||
carrierSelected = true
|
||||
}
|
||||
if transport == "" && !carrierSelected {
|
||||
transport = transportHint
|
||||
}
|
||||
if transport == "" {
|
||||
transport = provider.config.Transport
|
||||
}
|
||||
if transport == "" {
|
||||
transport = "tcp"
|
||||
}
|
||||
localAddress := provider.config.LocalAddress
|
||||
if localAddress == "" {
|
||||
if endpointIP := net.ParseIP(endpoint.host); endpointIP != nil && endpointIP.To4() == nil {
|
||||
localAddress = tunnel.LocalIPv6
|
||||
} else {
|
||||
localAddress = tunnel.LocalIPv4
|
||||
if strings.TrimSpace(localAddress) == "" {
|
||||
localAddress = tunnel.LocalIPv6
|
||||
}
|
||||
}
|
||||
}
|
||||
localAddress = strings.TrimSpace(strings.Split(localAddress, "/")[0])
|
||||
if localAddress == "" {
|
||||
return nil, errors.New("ims: tunnel did not provide a local address")
|
||||
}
|
||||
if !localAddressProvenByTunnel(localAddress, tunnel) {
|
||||
return nil, errors.New("ims: configured local address is not assigned by the SWu tunnel")
|
||||
}
|
||||
|
||||
transports := []string{transport}
|
||||
if provider.config.AutoTransportFallback {
|
||||
alternate := "udp"
|
||||
if transport == "udp" {
|
||||
alternate = "tcp"
|
||||
}
|
||||
transports = append(transports, alternate)
|
||||
}
|
||||
var lastErr error
|
||||
for attempt, candidate := range transports {
|
||||
connection, dialErr := dialSIP(ctx, candidate, localAddress, 0, endpoint.address())
|
||||
if dialErr != nil {
|
||||
lastErr = fmt.Errorf("ims: connect to P-CSCF over %s: %w", candidate, dialErr)
|
||||
if attempt+1 < len(transports) && ctx.Err() == nil {
|
||||
provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], lastErr)
|
||||
continue
|
||||
for pcscfIndex, pcscf := range pcscfCandidates {
|
||||
endpoint, transportHint, err := parsePCSCF(pcscf, provider.config.Port)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if provider.config.PCSCF != "" && !pcscfProvenByTunnel(endpoint, tunnel.PCSCF, provider.config.Port) {
|
||||
return nil, errors.New("ims: configured P-CSCF is not proven by the SWu tunnel")
|
||||
}
|
||||
transport, carrierSelected := carrierTransportForIdentity(provider.config, request.Identity)
|
||||
if cached := provider.cachedTransport(request.Identity); cached != "" {
|
||||
transport = cached
|
||||
carrierSelected = true
|
||||
}
|
||||
if transport == "" && !carrierSelected {
|
||||
transport = transportHint
|
||||
}
|
||||
if transport == "" {
|
||||
transport = provider.config.Transport
|
||||
}
|
||||
if transport == "" {
|
||||
transport = "tcp"
|
||||
}
|
||||
localAddress := provider.config.LocalAddress
|
||||
if localAddress == "" {
|
||||
if endpointIP := net.ParseIP(endpoint.host); endpointIP != nil && endpointIP.To4() == nil {
|
||||
localAddress = tunnel.LocalIPv6
|
||||
} else {
|
||||
localAddress = tunnel.LocalIPv4
|
||||
if strings.TrimSpace(localAddress) == "" {
|
||||
localAddress = tunnel.LocalIPv6
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
session, sessionErr := newSession(provider, request, identities, endpoint, candidate, connection)
|
||||
if sessionErr != nil {
|
||||
_ = connection.Close()
|
||||
return nil, sessionErr
|
||||
localAddress = strings.TrimSpace(strings.Split(localAddress, "/")[0])
|
||||
if localAddress == "" {
|
||||
return nil, errors.New("ims: tunnel did not provide a local address")
|
||||
}
|
||||
establishErr := session.establish(ctx)
|
||||
if establishErr == nil {
|
||||
provider.rememberTransport(request.Identity, candidate)
|
||||
if attempt > 0 {
|
||||
provider.config.Logger.Info("IMS automatic transport fallback succeeded",
|
||||
"carrier_profile", vowifi.ResolveCarrierProfile(request.Identity).ID,
|
||||
"transport", candidate)
|
||||
if !localAddressProvenByTunnel(localAddress, tunnel) {
|
||||
return nil, errors.New("ims: configured local address is not assigned by the SWu tunnel")
|
||||
}
|
||||
|
||||
transports := []string{transport}
|
||||
if provider.config.AutoTransportFallback {
|
||||
alternate := "udp"
|
||||
if transport == "udp" {
|
||||
alternate = "tcp"
|
||||
}
|
||||
return session, nil
|
||||
transports = append(transports, alternate)
|
||||
}
|
||||
sipResponseObserved := session.evidence.LastSIPCode != 0
|
||||
session.abort()
|
||||
lastErr = establishErr
|
||||
if sipResponseObserved || attempt+1 >= len(transports) || ctx.Err() != nil {
|
||||
return nil, lastErr
|
||||
for attempt, candidate := range transports {
|
||||
connection, dialErr := dialSIP(ctx, candidate, localAddress, 0, endpoint.address())
|
||||
if dialErr != nil {
|
||||
lastErr = fmt.Errorf("ims: connect to P-CSCF over %s: %w", candidate, dialErr)
|
||||
if attempt+1 < len(transports) && ctx.Err() == nil {
|
||||
provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], lastErr)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
session, sessionErr := newSession(provider, request, identities, endpoint, candidate, connection)
|
||||
if sessionErr != nil {
|
||||
_ = connection.Close()
|
||||
lastErr = sessionErr
|
||||
break
|
||||
}
|
||||
establishErr := session.establish(ctx)
|
||||
if establishErr == nil {
|
||||
provider.rememberTransport(request.Identity, candidate)
|
||||
if attempt > 0 || pcscfIndex > 0 {
|
||||
provider.config.Logger.Info("IMS automatic transport fallback succeeded",
|
||||
"carrier_profile", vowifi.ResolveCarrierProfile(request.Identity).ID,
|
||||
"transport", candidate)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
sipResponseObserved := session.evidence.LastSIPCode != 0
|
||||
session.abort()
|
||||
lastErr = establishErr
|
||||
if sipResponseObserved || attempt+1 >= len(transports) || ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], establishErr)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
provider.logTransportFallback(request.Identity, candidate, transports[attempt+1], establishErr)
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
@@ -384,8 +394,13 @@ func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet,
|
||||
if !digitsBetween(imsi, 5, 16) {
|
||||
return identitySet{}, errors.New("ims: SIM IMSI is unavailable or invalid")
|
||||
}
|
||||
mcc := strings.TrimSpace(identity.HomeMCC)
|
||||
mnc := strings.TrimSpace(identity.HomeMNC)
|
||||
profile := vowifi.ResolveCarrierProfile(identity)
|
||||
mcc := strings.TrimSpace(profile.RouteMCC)
|
||||
mnc := strings.TrimSpace(profile.RouteMNC)
|
||||
if mcc == "" || mnc == "" {
|
||||
mcc = strings.TrimSpace(identity.HomeMCC)
|
||||
mnc = strings.TrimSpace(identity.HomeMNC)
|
||||
}
|
||||
if !digitsBetween(mcc, 3, 3) || !digitsBetween(mnc, 2, 3) {
|
||||
return identitySet{}, errors.New("ims: home PLMN is unavailable or invalid")
|
||||
}
|
||||
@@ -395,7 +410,7 @@ func deriveIdentities(identity vowifi.SIMIdentity, config Config) (identitySet,
|
||||
domain := fmt.Sprintf("ims.mnc%s.mcc%s.3gppnetwork.org", mnc, mcc)
|
||||
privateDomain := domain
|
||||
publicDomain := domain
|
||||
if vowifi.ResolveCarrierProfile(identity).IMSIdentityProfile == vowifi.IMSProfileATT {
|
||||
if profile.IMSIdentityProfile == vowifi.IMSProfileATT {
|
||||
// AT&T provisions the IMPI and IMPU in its ISIM domains rather than
|
||||
// the generic 3GPP PLMN IMS domain.
|
||||
domain = "one.att.net"
|
||||
|
||||
@@ -71,7 +71,16 @@ func (media *rtpMedia) ready() bool {
|
||||
}
|
||||
|
||||
func (media *rtpMedia) offerSDP(local net.IP) []byte {
|
||||
return media.buildSDP(local, "8 0", nil)
|
||||
return media.buildSDP(local, "8 0 104 102 100", []string{
|
||||
"a=rtpmap:8 PCMA/8000",
|
||||
"a=rtpmap:0 PCMU/8000",
|
||||
"a=rtpmap:104 AMR-WB/16000",
|
||||
"a=fmtp:104 mode-change-capability=2;max-red=220",
|
||||
"a=rtpmap:102 AMR/8000",
|
||||
"a=fmtp:102 mode-change-capability=2;max-red=220",
|
||||
"a=rtpmap:100 telephone-event/8000",
|
||||
"a=fmtp:100 0-15",
|
||||
})
|
||||
}
|
||||
|
||||
func (media *rtpMedia) answerSDP(local net.IP) []byte {
|
||||
@@ -81,8 +90,12 @@ func (media *rtpMedia) answerSDP(local net.IP) []byte {
|
||||
if codec == "" {
|
||||
return media.offerSDP(local)
|
||||
}
|
||||
rate := 8000
|
||||
if codec == "AMR-WB" {
|
||||
rate = 16000
|
||||
}
|
||||
return media.buildSDP(local, strconv.Itoa(int(payload)), []string{
|
||||
fmt.Sprintf("a=rtpmap:%d %s/8000", payload, codec),
|
||||
fmt.Sprintf("a=rtpmap:%d %s/%d", payload, codec, rate),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,7 +123,16 @@ func (media *rtpMedia) buildSDP(local net.IP, formats string, attributes []strin
|
||||
fmt.Sprintf("m=audio %d RTP/AVP %s", port, formats),
|
||||
}
|
||||
if attributes == nil {
|
||||
lines = append(lines, "a=rtpmap:8 PCMA/8000", "a=rtpmap:0 PCMU/8000")
|
||||
lines = append(lines,
|
||||
"a=rtpmap:8 PCMA/8000",
|
||||
"a=rtpmap:0 PCMU/8000",
|
||||
"a=rtpmap:104 AMR-WB/16000",
|
||||
"a=fmtp:104 mode-change-capability=2;max-red=220",
|
||||
"a=rtpmap:102 AMR/8000",
|
||||
"a=fmtp:102 mode-change-capability=2;max-red=220",
|
||||
"a=rtpmap:100 telephone-event/8000",
|
||||
"a=fmtp:100 0-15",
|
||||
)
|
||||
} else {
|
||||
lines = append(lines, attributes...)
|
||||
}
|
||||
@@ -137,15 +159,24 @@ func (media *rtpMedia) configureRemote(body []byte) error {
|
||||
name = "PCMU"
|
||||
case 8:
|
||||
name = "PCMA"
|
||||
case 100:
|
||||
continue
|
||||
default:
|
||||
name = fmt.Sprintf("PAYLOAD-%d", parsed)
|
||||
}
|
||||
}
|
||||
if name == "PCMA" || name == "PCMU" {
|
||||
if name != "TELEPHONE-EVENT" {
|
||||
codec, payload = name, byte(parsed)
|
||||
break
|
||||
}
|
||||
}
|
||||
if codec == "" && len(formats) > 0 {
|
||||
if parsed, parseErr := strconv.Atoi(formats[0]); parseErr == nil {
|
||||
codec, payload = fmt.Sprintf("PAYLOAD-%d", parsed), byte(parsed)
|
||||
}
|
||||
}
|
||||
if codec == "" {
|
||||
return errors.New("ims: remote endpoint did not accept PCMA or PCMU audio")
|
||||
return errors.New("ims: remote SDP has no usable audio format")
|
||||
}
|
||||
media.mu.Lock()
|
||||
media.remote = &net.UDPAddr{IP: address, Port: port}
|
||||
|
||||
Reference in New Issue
Block a user