mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
feat: implement dynamic carrier profile system and infrastructure for VoWiFi configuration
This commit is contained in:
@@ -411,7 +411,7 @@ func ResolveCarrierProfile(identity SIMIdentity) CarrierProfile {
|
||||
continue
|
||||
}
|
||||
bestScore = score
|
||||
resolved = applyCarrierProfileRule(resolved, rule, source)
|
||||
resolved = applyCarrierProfileRule(resolved, rule, source, identity)
|
||||
}
|
||||
return resolved
|
||||
}
|
||||
@@ -441,16 +441,18 @@ func matchCarrierProfileRule(rule carrierProfileRule, identity SIMIdentity) (int
|
||||
func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int, string, bool) {
|
||||
score := 0
|
||||
sources := make([]string, 0, 6)
|
||||
hasHomePLMNMatch := false
|
||||
if len(match.HomePLMNs) > 0 {
|
||||
wanted := canonicalPLMN(identity.HomeMCC, identity.HomeMNC)
|
||||
if wanted == "" || !matchesAny(match.HomePLMNs, func(value string) bool {
|
||||
if wanted != "" && matchesAny(match.HomePLMNs, func(value string) bool {
|
||||
return canonicalPLMNValue(value) == wanted
|
||||
}) {
|
||||
return 0, "", false
|
||||
score += 100
|
||||
sources = append(sources, "hplmn")
|
||||
hasHomePLMNMatch = true
|
||||
}
|
||||
score += 100
|
||||
sources = append(sources, "hplmn")
|
||||
}
|
||||
hasSelectorMatch := false
|
||||
for _, selector := range []struct {
|
||||
name string
|
||||
weight int
|
||||
@@ -467,27 +469,32 @@ func matchCarrierProfile(match carrierProfileMatch, identity SIMIdentity) (int,
|
||||
continue
|
||||
}
|
||||
actual := strings.TrimSpace(selector.actual)
|
||||
if actual == "" || !matchesAny(selector.values, func(prefix string) bool {
|
||||
if actual != "" && matchesAny(selector.values, func(prefix string) bool {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if selector.foldCase {
|
||||
return strings.HasPrefix(strings.ToLower(actual), strings.ToLower(prefix))
|
||||
}
|
||||
return strings.HasPrefix(actual, prefix)
|
||||
}) {
|
||||
score += selector.weight
|
||||
sources = append(sources, selector.name)
|
||||
hasSelectorMatch = true
|
||||
} else if !hasHomePLMNMatch {
|
||||
return 0, "", false
|
||||
}
|
||||
score += selector.weight
|
||||
sources = append(sources, selector.name)
|
||||
}
|
||||
if len(match.SPNs) > 0 {
|
||||
spn := strings.TrimSpace(identity.SPN)
|
||||
if spn == "" || !matchesAny(match.SPNs, func(value string) bool {
|
||||
if spn != "" && matchesAny(match.SPNs, func(value string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(value), spn)
|
||||
}) {
|
||||
return 0, "", false
|
||||
score += 20
|
||||
sources = append(sources, "spn")
|
||||
hasSelectorMatch = true
|
||||
}
|
||||
score += 20
|
||||
sources = append(sources, "spn")
|
||||
}
|
||||
if !hasHomePLMNMatch && !hasSelectorMatch {
|
||||
return 0, "", false
|
||||
}
|
||||
return score, strings.Join(sources, "+"), score > 0
|
||||
}
|
||||
@@ -501,11 +508,43 @@ func matchesAny(values []string, match func(string) bool) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func applyCarrierProfileRule(base CarrierProfile, rule carrierProfileRule, source string) CarrierProfile {
|
||||
func applyCarrierProfileRule(base CarrierProfile, rule carrierProfileRule, source string, identity SIMIdentity) CarrierProfile {
|
||||
base.ID = rule.ID
|
||||
base.MatchSource = source
|
||||
base.RouteMCC = strings.TrimSpace(rule.Route.MCC)
|
||||
base.RouteMNC = strings.TrimSpace(rule.Route.MNC)
|
||||
if base.RouteMCC == "" {
|
||||
currentPLMN := canonicalPLMN(identity.HomeMCC, identity.HomeMNC)
|
||||
if currentPLMN != "" {
|
||||
for _, m := range append([]carrierProfileMatch{rule.Match}, rule.MatchAny...) {
|
||||
for _, plmn := range m.HomePLMNs {
|
||||
if canonicalPLMNValue(plmn) == currentPLMN {
|
||||
base.RouteMCC = strings.TrimSpace(identity.HomeMCC)
|
||||
base.RouteMNC = strings.TrimSpace(identity.HomeMNC)
|
||||
break
|
||||
}
|
||||
}
|
||||
if base.RouteMCC != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if base.RouteMCC == "" {
|
||||
for _, m := range append([]carrierProfileMatch{rule.Match}, rule.MatchAny...) {
|
||||
for _, plmn := range m.HomePLMNs {
|
||||
plmn = canonicalPLMNValue(plmn)
|
||||
if len(plmn) >= 5 {
|
||||
base.RouteMCC = plmn[:3]
|
||||
base.RouteMNC = plmn[3:]
|
||||
break
|
||||
}
|
||||
}
|
||||
if base.RouteMCC != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
base.EPDG = strings.ToLower(strings.TrimSpace(rule.EPDG.Hostname))
|
||||
if value := strings.TrimSpace(rule.IKE.Proposal); value != "" {
|
||||
base.IKEProposal = value
|
||||
@@ -647,19 +686,154 @@ func IsATT310280(identity SIMIdentity) bool {
|
||||
}
|
||||
|
||||
func applyAssignedCarrierRoute(identity SIMIdentity) SIMIdentity {
|
||||
if strings.TrimSpace(identity.EPDG) != "" {
|
||||
profile := ResolveCarrierProfile(identity)
|
||||
if profile.ID != CarrierProfileStandard && profile.RouteMCC != "" {
|
||||
identity.HomeMCC = profile.RouteMCC
|
||||
identity.HomeMNC = profile.RouteMNC
|
||||
if profile.EPDG != "" {
|
||||
identity.EPDG = profile.EPDG
|
||||
} else {
|
||||
identity.EPDG = standardEPDGHostname(profile.RouteMCC, profile.RouteMNC)
|
||||
}
|
||||
return identity
|
||||
}
|
||||
profile := ResolveCarrierProfile(identity)
|
||||
switch {
|
||||
case profile.EPDG != "":
|
||||
identity.EPDG = profile.EPDG
|
||||
case profile.RouteMCC != "":
|
||||
identity.EPDG = standardEPDGHostname(profile.RouteMCC, profile.RouteMNC)
|
||||
|
||||
if strings.TrimSpace(identity.ICCID) != "" {
|
||||
if mcc, mnc, ok := HomePLMNFromICCID(identity.ICCID); ok {
|
||||
imsiCountry := countryCodeForMCC(identity.HomeMCC)
|
||||
iccidCountry := countryCodeForMCC(mcc)
|
||||
if identity.HomeMCC == "" || (imsiCountry != "" && iccidCountry != "" && imsiCountry != iccidCountry) {
|
||||
identity.HomeMCC = mcc
|
||||
identity.HomeMNC = mnc
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(identity.EPDG) == "" && identity.HomeMCC != "" && identity.HomeMNC != "" {
|
||||
identity.EPDG = standardEPDGHostname(identity.HomeMCC, identity.HomeMNC)
|
||||
}
|
||||
return identity
|
||||
}
|
||||
|
||||
func countryCodeForMCC(mcc string) string {
|
||||
switch strings.TrimSpace(mcc) {
|
||||
case "515":
|
||||
return "PH"
|
||||
case "262":
|
||||
return "DE"
|
||||
case "204":
|
||||
return "NL"
|
||||
case "234", "235":
|
||||
return "GB"
|
||||
case "460":
|
||||
return "CN"
|
||||
case "454":
|
||||
return "HK"
|
||||
case "466", "467":
|
||||
return "TW"
|
||||
case "525":
|
||||
return "SG"
|
||||
case "440", "441":
|
||||
return "JP"
|
||||
case "450":
|
||||
return "KR"
|
||||
case "310", "311", "312", "313", "314", "315", "316":
|
||||
return "US"
|
||||
case "302":
|
||||
return "CA"
|
||||
case "505":
|
||||
return "AU"
|
||||
case "208":
|
||||
return "FR"
|
||||
case "214":
|
||||
return "ES"
|
||||
case "222":
|
||||
return "IT"
|
||||
case "228":
|
||||
return "CH"
|
||||
case "232":
|
||||
return "AT"
|
||||
case "206":
|
||||
return "BE"
|
||||
case "260":
|
||||
return "PL"
|
||||
case "520":
|
||||
return "TH"
|
||||
case "510":
|
||||
return "ID"
|
||||
case "502":
|
||||
return "MY"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// HomePLMNFromICCID infers the home MCC/MNC from well-known global ICCID prefixes.
|
||||
func HomePLMNFromICCID(iccid string) (mcc, mnc string, ok bool) {
|
||||
iccid = strings.TrimSpace(iccid)
|
||||
if len(iccid) < 6 || !strings.HasPrefix(iccid, "89") {
|
||||
return "", "", false
|
||||
}
|
||||
prefixes := []struct {
|
||||
prefix string
|
||||
mcc string
|
||||
mnc string
|
||||
}{
|
||||
// Philippines
|
||||
{"896366", "515", "66"}, // DITO
|
||||
{"896302", "515", "02"}, // Globe
|
||||
{"896303", "515", "03"}, // Smart
|
||||
// Germany
|
||||
{"894920", "262", "02"}, // Vodafone DE
|
||||
{"894901", "262", "01"}, // Telekom DE
|
||||
{"894902", "262", "03"}, // O2 DE
|
||||
{"894903", "262", "03"},
|
||||
{"894907", "262", "07"},
|
||||
// United Kingdom
|
||||
{"894410", "234", "15"}, // Vodafone UK
|
||||
{"894415", "234", "15"},
|
||||
{"894411", "234", "30"}, // EE
|
||||
{"894430", "234", "30"},
|
||||
{"894420", "234", "20"}, // Three UK
|
||||
{"894421", "234", "10"}, // O2 UK
|
||||
// Netherlands
|
||||
{"8937204", "204", "04"}, // Vodafone NL
|
||||
{"893104", "204", "04"},
|
||||
{"893108", "204", "08"}, // KPN
|
||||
{"893116", "204", "16"}, // Odido
|
||||
// Hong Kong
|
||||
{"8985201", "454", "00"}, // CSL
|
||||
{"8985203", "454", "03"}, // 3 HK
|
||||
{"898523", "454", "03"},
|
||||
{"8985204", "454", "12"}, // CMHK
|
||||
{"8985206", "454", "06"}, // SmarTone
|
||||
// China
|
||||
{"898600", "460", "00"}, // China Mobile
|
||||
{"898602", "460", "00"},
|
||||
{"898604", "460", "00"},
|
||||
{"898607", "460", "00"},
|
||||
{"898601", "460", "01"}, // China Unicom
|
||||
{"898606", "460", "01"},
|
||||
{"898609", "460", "01"},
|
||||
{"898603", "460", "03"}, // China Telecom
|
||||
{"898605", "460", "03"},
|
||||
{"898611", "460", "03"},
|
||||
// Taiwan
|
||||
{"8988601", "466", "92"}, // Chunghwa
|
||||
{"8988602", "466", "97"}, // Taiwan Mobile
|
||||
{"8988603", "466", "01"}, // FarEasTone
|
||||
// Singapore
|
||||
{"896501", "525", "01"}, // Singtel
|
||||
{"896502", "525", "05"}, // StarHub
|
||||
{"896503", "525", "03"}, // M1
|
||||
{"896504", "525", "10"}, // SIMBA
|
||||
}
|
||||
for _, entry := range prefixes {
|
||||
if strings.HasPrefix(iccid, entry.prefix) {
|
||||
return entry.mcc, entry.mnc, true
|
||||
}
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// EPDGDNSClientSubnet returns a deliberately scoped EDNS client subnet for an
|
||||
// ePDG whose authoritative DNS only exposes addresses to home-country
|
||||
// resolvers. An empty result means ordinary system DNS remains authoritative.
|
||||
@@ -672,6 +846,103 @@ func EPDGDNSClientSubnet(host string) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
if idx := strings.Index(host, ".mcc"); idx >= 0 && len(host) >= idx+7 {
|
||||
mcc := host[idx+4 : idx+7]
|
||||
if decimalString(mcc) {
|
||||
if subnet := MCCDefaultClientSubnet(mcc); subnet != "" {
|
||||
return subnet
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MCCDefaultClientSubnet returns the standard GeoDNS EDNS client subnet for a country MCC.
|
||||
func MCCDefaultClientSubnet(mcc string) string {
|
||||
switch strings.TrimSpace(mcc) {
|
||||
case "262": // Germany
|
||||
return "139.7.0.0/16"
|
||||
case "204": // Netherlands
|
||||
return "109.39.0.0/16"
|
||||
case "234", "235": // UK
|
||||
return "212.183.0.0/16"
|
||||
case "515": // Philippines
|
||||
return "112.198.0.0/16"
|
||||
case "454": // Hong Kong
|
||||
return "203.0.0.0/16"
|
||||
case "466", "467": // Taiwan
|
||||
return "210.0.0.0/16"
|
||||
case "525": // Singapore
|
||||
return "202.166.0.0/16"
|
||||
case "440", "441": // Japan
|
||||
return "126.0.0.0/16"
|
||||
case "450": // South Korea
|
||||
return "211.0.0.0/16"
|
||||
case "310", "311", "312", "313", "314", "315", "316": // USA
|
||||
return "198.228.0.0/16"
|
||||
case "302": // Canada
|
||||
return "142.0.0.0/16"
|
||||
case "505": // Australia
|
||||
return "1.120.0.0/16"
|
||||
case "520": // Thailand
|
||||
return "171.96.0.0/16"
|
||||
case "510": // Indonesia
|
||||
return "182.0.0.0/16"
|
||||
case "502": // Malaysia
|
||||
return "115.132.0.0/16"
|
||||
case "208": // France
|
||||
return "194.51.0.0/16"
|
||||
case "214": // Spain
|
||||
return "212.166.0.0/16"
|
||||
case "222": // Italy
|
||||
return "83.224.0.0/16"
|
||||
case "228": // Switzerland
|
||||
return "178.192.0.0/16"
|
||||
case "232": // Austria
|
||||
return "194.138.0.0/16"
|
||||
case "206": // Belgium
|
||||
return "193.190.0.0/16"
|
||||
case "260": // Poland
|
||||
return "83.0.0.0/16"
|
||||
case "268": // Portugal
|
||||
return "194.65.0.0/16"
|
||||
case "272": // Ireland
|
||||
return "193.1.0.0/16"
|
||||
case "238": // Denmark
|
||||
return "193.162.0.0/16"
|
||||
case "240": // Sweden
|
||||
return "194.236.0.0/16"
|
||||
case "242": // Norway
|
||||
return "193.69.0.0/16"
|
||||
case "244": // Finland
|
||||
return "193.64.0.0/16"
|
||||
case "202": // Greece
|
||||
return "194.219.0.0/16"
|
||||
case "216": // Hungary
|
||||
return "195.199.0.0/16"
|
||||
case "230": // Czech Republic
|
||||
return "195.113.0.0/16"
|
||||
case "286": // Turkey
|
||||
return "195.175.0.0/16"
|
||||
case "425": // Israel
|
||||
return "192.114.0.0/16"
|
||||
case "404", "405": // India
|
||||
return "103.0.0.0/16"
|
||||
case "655": // South Africa
|
||||
return "196.0.0.0/16"
|
||||
case "724": // Brazil
|
||||
return "177.0.0.0/16"
|
||||
case "334": // Mexico
|
||||
return "187.188.0.0/16"
|
||||
case "452": // Vietnam
|
||||
return "118.69.0.0/16"
|
||||
case "455": // Macao
|
||||
return "202.175.0.0/16"
|
||||
case "530": // New Zealand
|
||||
return "202.27.0.0/16"
|
||||
case "460": // China
|
||||
return "223.5.5.0/24"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"metadata": {
|
||||
"count": 651,
|
||||
"count": 652,
|
||||
"generated_at": "2026-08-19T17:12:07Z",
|
||||
"source": "dwilliamsuk/ios-carrier-bundles"
|
||||
},
|
||||
@@ -11611,7 +11611,35 @@
|
||||
"73003"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ipcc-dito-51566",
|
||||
"match": {
|
||||
"home_plmns": [
|
||||
"51566",
|
||||
"515066"
|
||||
],
|
||||
"iccid_prefixes": [
|
||||
"896366"
|
||||
],
|
||||
"spns": [
|
||||
"DITO"
|
||||
]
|
||||
},
|
||||
"route": {
|
||||
"mcc": "515",
|
||||
"mnc": "66"
|
||||
},
|
||||
"epdg": {
|
||||
"hostname": "epdg.epc.mnc066.mcc515.pub.3gppnetwork.org"
|
||||
},
|
||||
"ike": {
|
||||
"proposal": "modern"
|
||||
},
|
||||
"ims": {
|
||||
"ipsec_encryption": "aes-cbc"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ const (
|
||||
usimAIDPrefix = "A0000000871002"
|
||||
isimAIDPrefix = "A0000000871004"
|
||||
efADDecimal = 28589 // 0x6FAD
|
||||
efEHPLMNDecimal = 28441 // 0x6F19 (3GPP TS 31.102 EF_EHPLMN)
|
||||
channelCleanupTimeout = 3 * time.Second
|
||||
)
|
||||
|
||||
@@ -239,14 +240,79 @@ func (adapter *EC20Adapter) readHomePLMN(
|
||||
return mcc, mnc, nil
|
||||
}
|
||||
}
|
||||
// Exact assigned HPLMN prefixes are data, not an MNC-length heuristic. The
|
||||
// target Vodafone UK SIM is 234/15. Unknown assignments remain fail-closed.
|
||||
// Exact assigned HPLMN prefixes are data, not an MNC-length heuristic.
|
||||
if mcc, mnc, ok := assignedHomePLMN(imsi); ok {
|
||||
return mcc, mnc, nil
|
||||
}
|
||||
// 3GPP TS 31.102 Section 4.2.84: Query EF_EHPLMN (Equivalent Home PLMN).
|
||||
if ehplmns, err := adapter.readEHPLMN(ctx, deviceID); err == nil && len(ehplmns) > 0 {
|
||||
first := ehplmns[0]
|
||||
if len(first) >= 5 {
|
||||
return first[:3], first[3:], nil
|
||||
}
|
||||
}
|
||||
return "", "", efErr
|
||||
}
|
||||
|
||||
func (adapter *EC20Adapter) readEHPLMN(
|
||||
ctx context.Context,
|
||||
deviceID string,
|
||||
) ([]string, error) {
|
||||
commands := []string{
|
||||
fmt.Sprintf("AT+CRSM=176,%d,0,0,0", efEHPLMNDecimal),
|
||||
fmt.Sprintf("AT+CRSM=176,%d,0,0,12", efEHPLMNDecimal),
|
||||
}
|
||||
var lastErr error
|
||||
for _, command := range commands {
|
||||
response, err := adapter.execute(ctx, deviceID, command)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
data, err := parseCRSMData(response)
|
||||
if err != nil || len(data) < 3 {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
plmns := parsePLMNListFromBytes(data)
|
||||
if len(plmns) > 0 {
|
||||
return plmns, nil
|
||||
}
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = errors.New("vocat: EF_EHPLMN is empty or unavailable")
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
func parsePLMNListFromBytes(data []byte) []string {
|
||||
var plmns []string
|
||||
for i := 0; i+3 <= len(data); i += 3 {
|
||||
b1, b2, b3 := data[i], data[i+1], data[i+2]
|
||||
mcc1 := b1 & 0x0f
|
||||
mcc2 := (b1 >> 4) & 0x0f
|
||||
mcc3 := b2 & 0x0f
|
||||
mnc3 := (b2 >> 4) & 0x0f
|
||||
mnc1 := b3 & 0x0f
|
||||
mnc2 := (b3 >> 4) & 0x0f
|
||||
|
||||
if mcc1 > 9 || mcc2 > 9 || mcc3 > 9 || mnc1 > 9 || mnc2 > 9 {
|
||||
continue
|
||||
}
|
||||
mcc := fmt.Sprintf("%d%d%d", mcc1, mcc2, mcc3)
|
||||
var mnc string
|
||||
if mnc3 <= 9 {
|
||||
mnc = fmt.Sprintf("%d%d%d", mnc1, mnc2, mnc3)
|
||||
} else {
|
||||
mnc = fmt.Sprintf("%d%d", mnc1, mnc2)
|
||||
}
|
||||
if len(mcc) == 3 && (len(mnc) == 2 || len(mnc) == 3) {
|
||||
plmns = append(plmns, mcc+mnc)
|
||||
}
|
||||
}
|
||||
return plmns
|
||||
}
|
||||
|
||||
func assignedHomePLMN(imsi string) (mcc, mnc string, ok bool) {
|
||||
assignments := []struct {
|
||||
prefix string
|
||||
|
||||
@@ -28,48 +28,67 @@ func resolveEPDG(ctx context.Context, resolver *net.Resolver, host string) ([]ne
|
||||
if resolver == nil {
|
||||
resolver = net.DefaultResolver
|
||||
}
|
||||
addresses, systemErr := resolver.LookupIPAddr(ctx, host)
|
||||
if systemErr == nil && len(addresses) > 0 {
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), "."))
|
||||
subnet := vowifi.EPDGDNSClientSubnet(normalized)
|
||||
if subnet == "" {
|
||||
if systemErr != nil {
|
||||
return nil, systemErr
|
||||
}
|
||||
return nil, errors.New("ePDG did not resolve to an IP address")
|
||||
addresses, systemErr := resolver.LookupIPAddr(ctx, host)
|
||||
validSystemAddresses := filterValidPublicEPDGAddresses(addresses)
|
||||
if systemErr == nil && len(validSystemAddresses) > 0 {
|
||||
return validSystemAddresses, nil
|
||||
}
|
||||
|
||||
subnet := vowifi.EPDGDNSClientSubnet(normalized)
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
var fallbackErr error
|
||||
// Vodafone's authoritative response has a 60-second TTL and recursive
|
||||
// resolvers can briefly cache the global CNAME without its geo-restricted
|
||||
// address records. Stay inside the runtime's two-minute setup window and
|
||||
// wait through one complete negative-cache TTL so a single reconnect is
|
||||
// sufficient; users should not have to click Reconnect repeatedly.
|
||||
const fallbackAttempts = 13
|
||||
for attempt := 0; attempt < fallbackAttempts; attempt++ {
|
||||
|
||||
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, normalized, subnet)
|
||||
fallback, fallbackErr = resolveEPDGWithECS(ctx, client, googleDNSOverHTTPS, targetHost, subnet)
|
||||
if fallbackErr == nil && len(fallback) > 0 {
|
||||
return fallback, nil
|
||||
}
|
||||
if attempt+1 < fallbackAttempts {
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
if systemErr == nil {
|
||||
systemErr = errors.New("system DNS returned no IP addresses")
|
||||
systemErr = errors.New("system DNS returned no usable public IP addresses")
|
||||
}
|
||||
return nil, fmt.Errorf("system DNS failed (%v); geographic DNS fallback failed: %w", systemErr, fallbackErr)
|
||||
}
|
||||
|
||||
func filterValidPublicEPDGAddresses(addresses []net.IPAddr) []net.IPAddr {
|
||||
result := make([]net.IPAddr, 0, len(addresses))
|
||||
for _, addr := range addresses {
|
||||
if addr.IP == nil || addr.IP.IsLoopback() || addr.IP.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
result = append(result, addr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func alternate3GPPHostname(host string) string {
|
||||
const prefix = "epdg.epc.mnc"
|
||||
if !strings.HasPrefix(host, prefix) {
|
||||
return ""
|
||||
}
|
||||
rest := host[len(prefix):]
|
||||
dot := strings.Index(rest, ".")
|
||||
if dot <= 0 {
|
||||
return ""
|
||||
}
|
||||
mnc := rest[:dot]
|
||||
suffix := rest[dot:]
|
||||
if len(mnc) == 3 && strings.HasPrefix(mnc, "0") {
|
||||
return prefix + mnc[1:] + suffix
|
||||
}
|
||||
if len(mnc) == 2 {
|
||||
return prefix + "0" + mnc + suffix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveEPDGWithECS(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
@@ -85,7 +104,9 @@ func resolveEPDGWithECS(
|
||||
query := parsed.Query()
|
||||
query.Set("name", strings.TrimSpace(host))
|
||||
query.Set("type", "A")
|
||||
query.Set("edns_client_subnet", strings.TrimSpace(subnet))
|
||||
if strings.TrimSpace(subnet) != "" {
|
||||
query.Set("edns_client_subnet", strings.TrimSpace(subnet))
|
||||
}
|
||||
parsed.RawQuery = query.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil)
|
||||
@@ -116,7 +137,7 @@ func resolveEPDGWithECS(
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(strings.TrimSuffix(strings.TrimSpace(answer.Data), "."))
|
||||
if ip == nil {
|
||||
if ip == nil || ip.IsLoopback() || ip.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
duplicate := false
|
||||
|
||||
@@ -120,20 +120,20 @@ func TestDeriveEPDGUsesExplicitPLMNAndNeverIMSIHeuristics(t *testing.T) {
|
||||
name: "three digit MNC is preserved",
|
||||
identity: SIMIdentity{
|
||||
ICCID: "one",
|
||||
HomeMCC: "310",
|
||||
HomeMCC: "999",
|
||||
HomeMNC: "260",
|
||||
},
|
||||
want: "epdg.epc.mnc260.mcc310.pub.3gppnetwork.org",
|
||||
want: "epdg.epc.mnc260.mcc999.pub.3gppnetwork.org",
|
||||
},
|
||||
{
|
||||
name: "AT&T 310280 uses standard 3GPP FQDN",
|
||||
name: "AT&T 310280 uses carrier bundle ePDG",
|
||||
identity: SIMIdentity{
|
||||
ICCID: "8901000000000000001",
|
||||
IMSI: "310280000000001",
|
||||
HomeMCC: "310",
|
||||
HomeMNC: "280",
|
||||
},
|
||||
want: "epdg.epc.mnc280.mcc310.pub.3gppnetwork.org",
|
||||
want: "epdg.epc.att.net",
|
||||
},
|
||||
{
|
||||
name: "explicit endpoint",
|
||||
|
||||
Reference in New Issue
Block a user