mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-20 14:53:42 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cba13634a | ||
|
|
b8df7f43f8 | ||
|
|
497cd24c8d |
+28
-30
@@ -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
|
||||
}
|
||||
@@ -1071,15 +1083,14 @@ func (manager *Manager) renameCachedProfile(id, iccid, nickname string) {
|
||||
manager.esimCacheMu.Unlock()
|
||||
}
|
||||
|
||||
// recoverAfterProfileSwitch owns the post-commit reset independently of the
|
||||
// initiating HTTP request. EC20 commonly drops the AT port while processing
|
||||
// CFUN=1,1, so the reset error is intentionally followed by discovery retries.
|
||||
// recoverAfterProfileSwitch owns the post-commit SIM reset independently of the
|
||||
// initiating HTTP request.
|
||||
func (manager *Manager) recoverAfterProfileSwitch(id string) {
|
||||
resetContext, cancelReset := context.WithTimeout(context.Background(), manager.longTimeout)
|
||||
if native, err := manager.powerCycleNativeQMISIM(resetContext, id); native {
|
||||
cancelReset()
|
||||
if err == nil {
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
// Native WWAN identity and profile verification are both QMI-backed.
|
||||
// Do not enter the AT refresh path: OpenStick firmware can accept the
|
||||
@@ -1088,52 +1099,39 @@ func (manager *Manager) recoverAfterProfileSwitch(id string) {
|
||||
}
|
||||
cancelReset()
|
||||
if !manager.isPCSCDevice(id) {
|
||||
resetContext, cancelReset := context.WithTimeout(context.Background(), manager.longTimeout)
|
||||
_ = manager.rebootForProfileSwitch(resetContext, id)
|
||||
resetContext, cancelReset := context.WithTimeout(context.Background(), manager.commandTimeout*2)
|
||||
_ = manager.softResetForProfileSwitch(resetContext, id)
|
||||
cancelReset()
|
||||
}
|
||||
manager.refreshAfterProfileSwitch(id)
|
||||
}
|
||||
|
||||
// refreshAfterProfileSwitch repopulates the device snapshot in the background
|
||||
// after an eSIM profile switch + modem reboot. /overview only serves the cached
|
||||
// snapshot, and nothing else live-reads post-switch, so without this the card
|
||||
// stays on "--" forever. The EC20 takes ~10-15s to come back from AT+CFUN=1,1,
|
||||
// so we delay first, then retry with backoff. Transport errors during the
|
||||
// reboot window are fine — Fix 1 discards the poisoned client and reopens on
|
||||
// the next attempt. All errors are swallowed: this is best-effort self-healing
|
||||
// and setResult already records the last failure for the UI.
|
||||
// after an eSIM profile switch.
|
||||
func (manager *Manager) refreshAfterProfileSwitch(id string) {
|
||||
if manager.isPCSCDevice(id) {
|
||||
time.Sleep(750 * time.Millisecond)
|
||||
for attempt := 0; attempt < 10; attempt++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*4)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*2)
|
||||
_, _ = manager.Discover(ctx)
|
||||
_, err := manager.Refresh(ctx, id)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return
|
||||
}
|
||||
const (
|
||||
settle = 8 * time.Second
|
||||
interval = 4 * time.Second
|
||||
attempts = 6
|
||||
settle = 1 * time.Second
|
||||
interval = 1 * time.Second
|
||||
attempts = 5
|
||||
)
|
||||
time.Sleep(settle)
|
||||
for attempt := 0; attempt < attempts; attempt++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*4)
|
||||
_, _ = manager.Discover(ctx)
|
||||
_, flightErr := manager.SetFlight(ctx, id, true)
|
||||
var err error
|
||||
if flightErr == nil {
|
||||
_, err = manager.Refresh(ctx, id)
|
||||
} else {
|
||||
err = flightErr
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), manager.commandTimeout*2)
|
||||
_, err := manager.Refresh(ctx, id)
|
||||
cancel()
|
||||
if err == nil {
|
||||
return
|
||||
@@ -1233,7 +1231,7 @@ func (manager *Manager) canVerifyProfileSwitchWithoutRestart(id string) bool {
|
||||
// is finalized by REFRESH/reset. The UI must not report success until the modem
|
||||
// is actually exposing the requested ICCID.
|
||||
func (manager *Manager) verifySwitchedICCID(ctx context.Context, id, expected string) error {
|
||||
return manager.verifySwitchedICCIDAttempts(ctx, id, expected, 6, 2*time.Second)
|
||||
return manager.verifySwitchedICCIDAttempts(ctx, id, expected, 6, 1*time.Second)
|
||||
}
|
||||
|
||||
func (manager *Manager) verifySwitchedICCIDAttempts(
|
||||
|
||||
+22
-13
@@ -631,13 +631,11 @@ func (manager *Manager) Reboot(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// rebootForProfileSwitch is the post-EnableProfile modem reset. After the eUICC
|
||||
// marks a new profile active, the modem keeps the old SIM cached and lands in
|
||||
// SIM failure (-CME 13) until it is bounced. ESIMSwitchProfile has already
|
||||
// released opMu by the time it calls this, so the reset is safe to take the
|
||||
// lock. This mirrors Reboot but is separate so the call site can't recurse into
|
||||
// a guarded-reset path.
|
||||
func (manager *Manager) rebootForProfileSwitch(ctx context.Context, id string) error {
|
||||
// softResetForProfileSwitch resets the baseband SIM stack using a soft CFUN sequence
|
||||
// (AT+CFUN=0 -> AT+CFUN=1/4) instead of rebooting the entire hardware module (AT+CFUN=1,1).
|
||||
// This causes the baseband to reload the new eSIM profile files within ~1-2 seconds
|
||||
// without disconnecting USB/PCIe or dropping serial communication ports.
|
||||
func (manager *Manager) softResetForProfileSwitch(ctx context.Context, id string) error {
|
||||
state, err := manager.lookup(id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -652,14 +650,25 @@ func (manager *Manager) rebootForProfileSwitch(ctx context.Context, id string) e
|
||||
manager.setResult(id, state, nil, err)
|
||||
return err
|
||||
}
|
||||
commandCtx, cancel := manager.withTimeout(ctx, manager.longTimeout)
|
||||
commandCtx, cancel := manager.withTimeout(ctx, manager.commandTimeout)
|
||||
defer cancel()
|
||||
_, err = client.Execute(commandCtx, "AT+CFUN=1,1")
|
||||
if closeErr := client.Close(); err == nil {
|
||||
err = closeErr
|
||||
|
||||
// 1. Cycle SIM interface to minimum functionality / clear cached SIM files
|
||||
_, _ = client.Execute(commandCtx, "AT+CFUN=0")
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
state.client = nil
|
||||
state.preFlightMode = nil
|
||||
|
||||
// 2. Restore radio to trigger fresh USIM file reading
|
||||
targetCFUN := "AT+CFUN=1"
|
||||
if state.snapshot != nil && state.snapshot.FlightMode {
|
||||
targetCFUN = "AT+CFUN=4"
|
||||
}
|
||||
_, err = client.Execute(commandCtx, targetCFUN)
|
||||
|
||||
manager.clearSnapshot(id, state)
|
||||
manager.setResult(id, state, nil, err)
|
||||
return 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": {
|
||||
|
||||
@@ -280,6 +280,239 @@ func decryptPayloads(
|
||||
return header, payloads, nil
|
||||
}
|
||||
|
||||
const defaultIKEFragmentSize = 1100
|
||||
|
||||
func encryptPayloadsFragmented(
|
||||
header ikeHeader,
|
||||
inner []payload,
|
||||
suite negotiatedSuite,
|
||||
encryptionKey []byte,
|
||||
integrityKey []byte,
|
||||
maxFragmentSize int,
|
||||
random io.Reader,
|
||||
) ([][]byte, error) {
|
||||
if random == nil {
|
||||
random = rand.Reader
|
||||
}
|
||||
if maxFragmentSize <= 0 {
|
||||
maxFragmentSize = defaultIKEFragmentSize
|
||||
}
|
||||
first, plaintext, err := marshalPayloadChain(inner)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ike: initialize AES: %w", err)
|
||||
}
|
||||
_, checksumLength, err := suite.integrityLengths()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maxChunk := maxFragmentSize - ikeHeaderLength - 8 - block.BlockSize() - block.BlockSize() - checksumLength
|
||||
if maxChunk < 64 {
|
||||
maxChunk = 64
|
||||
}
|
||||
|
||||
var chunks [][]byte
|
||||
for len(plaintext) > 0 {
|
||||
take := len(plaintext)
|
||||
if take > maxChunk {
|
||||
take = maxChunk
|
||||
}
|
||||
chunks = append(chunks, plaintext[:take])
|
||||
plaintext = plaintext[take:]
|
||||
}
|
||||
totalFragments := uint16(len(chunks))
|
||||
if totalFragments == 0 {
|
||||
totalFragments = 1
|
||||
chunks = [][]byte{nil}
|
||||
}
|
||||
|
||||
var packets [][]byte
|
||||
for index, chunk := range chunks {
|
||||
fragNum := uint16(index + 1)
|
||||
fragNext := uint8(payloadNone)
|
||||
if fragNum == 1 {
|
||||
fragNext = first
|
||||
}
|
||||
|
||||
paddingLength := block.BlockSize() - (len(chunk)+1)%block.BlockSize()
|
||||
if paddingLength == block.BlockSize() {
|
||||
paddingLength = 0
|
||||
}
|
||||
padding := make([]byte, paddingLength)
|
||||
if _, err := io.ReadFull(random, padding); err != nil {
|
||||
return nil, fmt.Errorf("ike: generate encrypted payload padding: %w", err)
|
||||
}
|
||||
paddedChunk := append(append([]byte(nil), chunk...), padding...)
|
||||
paddedChunk = append(paddedChunk, byte(paddingLength))
|
||||
|
||||
iv := make([]byte, block.BlockSize())
|
||||
if _, err := io.ReadFull(random, iv); err != nil {
|
||||
return nil, fmt.Errorf("ike: generate encrypted payload IV: %w", err)
|
||||
}
|
||||
ciphertext := make([]byte, len(paddedChunk))
|
||||
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, paddedChunk)
|
||||
|
||||
skfLength := 4 + 4 + len(iv) + len(ciphertext) + checksumLength
|
||||
if skfLength > 65535 {
|
||||
return nil, errors.New("ike: encrypted fragment exceeds 65535 bytes")
|
||||
}
|
||||
|
||||
body := make([]byte, skfLength)
|
||||
body[0] = fragNext
|
||||
body[1] = 0
|
||||
binary.BigEndian.PutUint16(body[2:4], uint16(skfLength))
|
||||
binary.BigEndian.PutUint16(body[4:6], fragNum)
|
||||
binary.BigEndian.PutUint16(body[6:8], totalFragments)
|
||||
copy(body[8:], iv)
|
||||
copy(body[8+len(iv):], ciphertext)
|
||||
|
||||
fragHeader := header
|
||||
fragHeader.NextPayload = payloadEncryptedFragment
|
||||
packet := fragHeader.marshal(body)
|
||||
checksum, err := integrityMAC(suite, integrityKey, packet[:len(packet)-checksumLength])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copy(packet[len(packet)-checksumLength:], checksum)
|
||||
packets = append(packets, packet)
|
||||
}
|
||||
return packets, nil
|
||||
}
|
||||
|
||||
func decryptSingleFragment(
|
||||
packet []byte,
|
||||
suite negotiatedSuite,
|
||||
encryptionKey []byte,
|
||||
integrityKey []byte,
|
||||
) (ikeHeader, uint8, uint16, uint16, []byte, error) {
|
||||
header, body, err := parseIKEPacket(packet)
|
||||
if err != nil {
|
||||
return ikeHeader{}, 0, 0, 0, nil, err
|
||||
}
|
||||
if header.NextPayload != payloadEncryptedFragment || len(body) < 8 {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: message is not an encrypted IKE fragment", errUnexpectedPacket)
|
||||
}
|
||||
skfLength := int(binary.BigEndian.Uint16(body[2:4]))
|
||||
if skfLength != len(body) {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: encrypted fragment length mismatch", errMalformedPacket)
|
||||
}
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("ike: initialize AES: %w", err)
|
||||
}
|
||||
_, checksumLength, err := suite.integrityLengths()
|
||||
if err != nil {
|
||||
return ikeHeader{}, 0, 0, 0, nil, err
|
||||
}
|
||||
if len(body) < 8+block.BlockSize()+block.BlockSize()+checksumLength {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: encrypted fragment is too short", errMalformedPacket)
|
||||
}
|
||||
expected, err := integrityMAC(suite, integrityKey, packet[:len(packet)-checksumLength])
|
||||
if err != nil {
|
||||
return ikeHeader{}, 0, 0, 0, nil, err
|
||||
}
|
||||
actual := packet[len(packet)-checksumLength:]
|
||||
if subtle.ConstantTimeCompare(actual, expected) != 1 {
|
||||
return ikeHeader{}, 0, 0, 0, nil, errIntegrityMismatch
|
||||
}
|
||||
|
||||
fragNext := body[0]
|
||||
fragNum := binary.BigEndian.Uint16(body[4:6])
|
||||
totalFrags := binary.BigEndian.Uint16(body[6:8])
|
||||
if fragNum == 0 || totalFrags == 0 || fragNum > totalFrags {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: invalid fragment numbers %d/%d", errMalformedPacket, fragNum, totalFrags)
|
||||
}
|
||||
|
||||
ivStart := 8
|
||||
ciphertextStart := ivStart + block.BlockSize()
|
||||
ciphertextEnd := len(body) - checksumLength
|
||||
ciphertext := body[ciphertextStart:ciphertextEnd]
|
||||
if len(ciphertext) == 0 || len(ciphertext)%block.BlockSize() != 0 {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: fragment ciphertext is not block aligned", errMalformedPacket)
|
||||
}
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
cipher.NewCBCDecrypter(block, body[ivStart:ciphertextStart]).CryptBlocks(plaintext, ciphertext)
|
||||
paddingLength := int(plaintext[len(plaintext)-1])
|
||||
if paddingLength+1 > len(plaintext) {
|
||||
return ikeHeader{}, 0, 0, 0, nil, fmt.Errorf("%w: invalid encrypted fragment padding", errMalformedPacket)
|
||||
}
|
||||
plaintext = plaintext[:len(plaintext)-paddingLength-1]
|
||||
return header, fragNext, fragNum, totalFrags, plaintext, nil
|
||||
}
|
||||
|
||||
func decryptPayloadsAny(
|
||||
packet []byte,
|
||||
fragments [][]byte,
|
||||
suite negotiatedSuite,
|
||||
encryptionKey []byte,
|
||||
integrityKey []byte,
|
||||
) (ikeHeader, []payload, error) {
|
||||
if len(fragments) > 0 {
|
||||
var (
|
||||
firstHeader ikeHeader
|
||||
firstNext uint8
|
||||
totalExpected uint16
|
||||
plaintexts = make(map[uint16][]byte)
|
||||
)
|
||||
for _, fragPacket := range fragments {
|
||||
hdr, next, num, total, plain, err := decryptSingleFragment(fragPacket, suite, encryptionKey, integrityKey)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
if totalExpected == 0 {
|
||||
firstHeader = hdr
|
||||
totalExpected = total
|
||||
} else if total != totalExpected || hdr.MessageID != firstHeader.MessageID || hdr.Exchange != firstHeader.Exchange {
|
||||
return ikeHeader{}, nil, fmt.Errorf("%w: inconsistent fragment headers", errMalformedPacket)
|
||||
}
|
||||
if num == 1 {
|
||||
firstNext = next
|
||||
}
|
||||
plaintexts[num] = plain
|
||||
}
|
||||
if uint16(len(plaintexts)) != totalExpected {
|
||||
return ikeHeader{}, nil, fmt.Errorf("%w: missing fragments: received %d of %d", errMalformedPacket, len(plaintexts), totalExpected)
|
||||
}
|
||||
var fullPlaintext []byte
|
||||
for i := uint16(1); i <= totalExpected; i++ {
|
||||
chunk, ok := plaintexts[i]
|
||||
if !ok {
|
||||
return ikeHeader{}, nil, fmt.Errorf("%w: missing fragment %d", errMalformedPacket, i)
|
||||
}
|
||||
fullPlaintext = append(fullPlaintext, chunk...)
|
||||
}
|
||||
payloads, err := parsePayloadChain(firstNext, fullPlaintext)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
return firstHeader, payloads, nil
|
||||
}
|
||||
|
||||
header, _, err := parseIKEPacket(packet)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
if header.NextPayload == payloadEncryptedFragment {
|
||||
hdr, next, num, total, plain, err := decryptSingleFragment(packet, suite, encryptionKey, integrityKey)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
if num != 1 || total != 1 {
|
||||
return ikeHeader{}, nil, fmt.Errorf("%w: standalone fragment with total=%d", errMalformedPacket, total)
|
||||
}
|
||||
payloads, err := parsePayloadChain(next, plain)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
return hdr, payloads, nil
|
||||
}
|
||||
return decryptPayloads(packet, suite, encryptionKey, integrityKey)
|
||||
}
|
||||
|
||||
var modpPrimes = map[uint16]string{
|
||||
dhMODP1024: "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" +
|
||||
"29024E088A67CC74020BBEA63B139B22514A08798E3404DD" +
|
||||
|
||||
@@ -113,3 +113,80 @@ func TestIKEKeyDerivationSeparatesDirections(t *testing.T) {
|
||||
t.Fatal("initiator and responder keys were not separated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRFC7383FragmentationAndReassembly(t *testing.T) {
|
||||
suite := legacyTestSuite()
|
||||
encryptionKey := bytes.Repeat([]byte{0x11}, 16)
|
||||
integrityKey := bytes.Repeat([]byte{0x22}, 20)
|
||||
header := ikeHeader{
|
||||
InitiatorSPI: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
ResponderSPI: [8]byte{8, 7, 6, 5, 4, 3, 2, 1},
|
||||
Exchange: exchangeIKEAuth,
|
||||
Flags: flagInitiator,
|
||||
MessageID: 9,
|
||||
}
|
||||
|
||||
largeCertData := bytes.Repeat([]byte{0xAB, 0xCD, 0xEF, 0x01}, 400) // 1600 bytes
|
||||
inner := []payload{
|
||||
{Type: payloadIDi, Body: []byte{3, 0, 0, 0, 'u', 's', 'e', 'r'}},
|
||||
{Type: payloadCert, Body: largeCertData},
|
||||
{Type: payloadAuth, Body: bytes.Repeat([]byte{0x55}, 64)},
|
||||
}
|
||||
|
||||
// Fragment into chunks with max fragment size 600 bytes
|
||||
packets, err := encryptPayloadsFragmented(
|
||||
header,
|
||||
inner,
|
||||
suite,
|
||||
encryptionKey,
|
||||
integrityKey,
|
||||
600,
|
||||
bytes.NewReader(bytes.Repeat([]byte{0x77}, 1024)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("encryptPayloadsFragmented() error = %v", err)
|
||||
}
|
||||
|
||||
if len(packets) < 3 {
|
||||
t.Fatalf("expected at least 3 fragments for large payload, got %d", len(packets))
|
||||
}
|
||||
|
||||
for i, pkt := range packets {
|
||||
hdr, body, parseErr := parseIKEPacket(pkt)
|
||||
if parseErr != nil {
|
||||
t.Fatalf("fragment %d parse error: %v", i+1, parseErr)
|
||||
}
|
||||
if hdr.NextPayload != payloadEncryptedFragment {
|
||||
t.Fatalf("fragment %d NextPayload = %d, want %d (payloadEncryptedFragment)", i+1, hdr.NextPayload, payloadEncryptedFragment)
|
||||
}
|
||||
if len(body) < 8 {
|
||||
t.Fatalf("fragment %d body too short", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt and reassemble
|
||||
decodedHeader, decoded, err := decryptPayloadsAny(nil, packets, suite, encryptionKey, integrityKey)
|
||||
if err != nil {
|
||||
t.Fatalf("decryptPayloadsAny() error = %v", err)
|
||||
}
|
||||
|
||||
if decodedHeader.MessageID != header.MessageID || len(decoded) != len(inner) {
|
||||
t.Fatalf("reassembled payload mismatch: header=%#v, count=%d, want=%d", decodedHeader, len(decoded), len(inner))
|
||||
}
|
||||
|
||||
for index := range inner {
|
||||
if decoded[index].Type != inner[index].Type || !bytes.Equal(decoded[index].Body, inner[index].Body) {
|
||||
t.Fatalf("decoded payload %d = %#v, want %#v", index, decoded[index], inner[index])
|
||||
}
|
||||
}
|
||||
|
||||
// Test tamper detection on second fragment
|
||||
tamperedPackets := make([][]byte, len(packets))
|
||||
for i := range packets {
|
||||
tamperedPackets[i] = append([]byte(nil), packets[i]...)
|
||||
}
|
||||
tamperedPackets[1][len(tamperedPackets[1])-1] ^= 0x55
|
||||
if _, _, err := decryptPayloadsAny(nil, tamperedPackets, suite, encryptionKey, integrityKey); !errors.Is(err, errIntegrityMismatch) {
|
||||
t.Fatalf("tampered fragment decrypt error = %v, want errIntegrityMismatch", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+103
-32
@@ -187,6 +187,7 @@ func (provider *Provider) start(ctx context.Context, request vowifi.TunnelReques
|
||||
{Type: payloadNonce, Body: initiatorNonce},
|
||||
makeNotify(notifyNATSource, sourceHash),
|
||||
makeNotify(notifyNATDestination, destinationHash),
|
||||
makeNotify(notifyFragmentationSupported, nil),
|
||||
}
|
||||
var (
|
||||
initRequest []byte
|
||||
@@ -243,6 +244,7 @@ func (provider *Provider) start(ctx context.Context, request vowifi.TunnelReques
|
||||
}
|
||||
break
|
||||
}
|
||||
peerSupportsFragmentation := hasNotifyType(initResponsePayloads, notifyFragmentationSupported)
|
||||
saPayload, err := onePayload(initResponsePayloads, payloadSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -345,21 +347,19 @@ func (provider *Provider) start(ctx context.Context, request vowifi.TunnelReques
|
||||
Flags: flagInitiator,
|
||||
MessageID: 1,
|
||||
}
|
||||
authRequest, err := encryptPayloads(authHeader, firstAuthPayloads, ikeSuite, keys.SKei, keys.SKai, provider.config.Random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authResponse, err := transport.RoundTrip(ctx, authRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authResponseHeader, authResponsePayloads, err := decryptAndValidate(
|
||||
authResponse, initiatorSPI, responseHeader.ResponderSPI, exchangeIKEAuth, 1, ikeSuite, keys,
|
||||
_, authResponsePayloads, err := sendAndReceiveIKEPayloads(
|
||||
ctx,
|
||||
transport,
|
||||
authHeader,
|
||||
firstAuthPayloads,
|
||||
ikeSuite,
|
||||
keys,
|
||||
peerSupportsFragmentation,
|
||||
provider.config.Random,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = authResponseHeader
|
||||
serverName := strings.TrimSpace(provider.config.ServerName)
|
||||
if serverName == "" {
|
||||
serverName = epdg
|
||||
@@ -409,27 +409,27 @@ func (provider *Provider) start(ctx context.Context, request vowifi.TunnelReques
|
||||
requestPayloads = append(requestPayloads, deviceIdentity)
|
||||
}
|
||||
}
|
||||
eapRequest, err := encryptPayloads(ikeHeader{
|
||||
eapHeader := ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responseHeader.ResponderSPI,
|
||||
Exchange: exchangeIKEAuth,
|
||||
Flags: flagInitiator,
|
||||
MessageID: messageID,
|
||||
}, requestPayloads, ikeSuite, keys.SKei, keys.SKai, provider.config.Random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if requested, notifyErr := deviceIdentityRequested(currentPayloads); notifyErr != nil {
|
||||
return nil, notifyErr
|
||||
} else if requested {
|
||||
deviceIdentityPending = true
|
||||
}
|
||||
eapResponse, err := transport.RoundTrip(ctx, eapRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, currentPayloads, err = decryptAndValidate(
|
||||
eapResponse, initiatorSPI, responseHeader.ResponderSPI, exchangeIKEAuth, messageID, ikeSuite, keys,
|
||||
_, currentPayloads, err = sendAndReceiveIKEPayloads(
|
||||
ctx,
|
||||
transport,
|
||||
eapHeader,
|
||||
requestPayloads,
|
||||
ikeSuite,
|
||||
keys,
|
||||
peerSupportsFragmentation,
|
||||
provider.config.Random,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -454,22 +454,22 @@ func (provider *Provider) start(ctx context.Context, request vowifi.TunnelReques
|
||||
}
|
||||
messageID++
|
||||
cleanupMessageID = messageID + 1
|
||||
finalRequest, err := encryptPayloads(ikeHeader{
|
||||
finalHeader := ikeHeader{
|
||||
InitiatorSPI: initiatorSPI,
|
||||
ResponderSPI: responseHeader.ResponderSPI,
|
||||
Exchange: exchangeIKEAuth,
|
||||
Flags: flagInitiator,
|
||||
MessageID: messageID,
|
||||
}, []payload{initiatorAUTH}, ikeSuite, keys.SKei, keys.SKai, provider.config.Random)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
finalResponse, err := transport.RoundTrip(ctx, finalRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, finalPayloads, err := decryptAndValidate(
|
||||
finalResponse, initiatorSPI, responseHeader.ResponderSPI, exchangeIKEAuth, messageID, ikeSuite, keys,
|
||||
_, finalPayloads, err := sendAndReceiveIKEPayloads(
|
||||
ctx,
|
||||
transport,
|
||||
finalHeader,
|
||||
[]payload{initiatorAUTH},
|
||||
ikeSuite,
|
||||
keys,
|
||||
peerSupportsFragmentation,
|
||||
provider.config.Random,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -763,7 +763,7 @@ func decryptAndValidate(
|
||||
suite negotiatedSuite,
|
||||
keys ikeKeys,
|
||||
) (ikeHeader, []payload, error) {
|
||||
header, payloads, err := decryptPayloads(packet, suite, keys.SKer, keys.SKar)
|
||||
header, payloads, err := decryptPayloadsAny(packet, nil, suite, keys.SKer, keys.SKar)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
@@ -778,6 +778,77 @@ func decryptAndValidate(
|
||||
return header, payloads, nil
|
||||
}
|
||||
|
||||
func decryptAndValidateFragments(
|
||||
packets [][]byte,
|
||||
initiatorSPI [8]byte,
|
||||
responderSPI [8]byte,
|
||||
exchange uint8,
|
||||
messageID uint32,
|
||||
suite negotiatedSuite,
|
||||
keys ikeKeys,
|
||||
) (ikeHeader, []payload, error) {
|
||||
if len(packets) == 0 {
|
||||
return ikeHeader{}, nil, errors.New("ike: empty exchange response")
|
||||
}
|
||||
if len(packets) == 1 {
|
||||
return decryptAndValidate(packets[0], initiatorSPI, responderSPI, exchange, messageID, suite, keys)
|
||||
}
|
||||
header, payloads, err := decryptPayloadsAny(nil, packets, suite, keys.SKer, keys.SKar)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
if header.InitiatorSPI != initiatorSPI ||
|
||||
header.ResponderSPI != responderSPI ||
|
||||
header.Exchange != exchange ||
|
||||
header.MessageID != messageID ||
|
||||
header.Flags&flagResponse == 0 ||
|
||||
header.Flags&flagInitiator != 0 {
|
||||
return ikeHeader{}, nil, fmt.Errorf("%w: encrypted response header does not match the request", errUnexpectedPacket)
|
||||
}
|
||||
return header, payloads, nil
|
||||
}
|
||||
|
||||
func sendAndReceiveIKEPayloads(
|
||||
ctx context.Context,
|
||||
transport datagramTransport,
|
||||
header ikeHeader,
|
||||
payloads []payload,
|
||||
suite negotiatedSuite,
|
||||
keys ikeKeys,
|
||||
peerSupportsFragmentation bool,
|
||||
random io.Reader,
|
||||
) (ikeHeader, []payload, error) {
|
||||
var outboundPackets [][]byte
|
||||
var err error
|
||||
if peerSupportsFragmentation {
|
||||
outboundPackets, err = encryptPayloadsFragmented(header, payloads, suite, keys.SKei, keys.SKai, defaultIKEFragmentSize, random)
|
||||
} else {
|
||||
pkt, encryptErr := encryptPayloads(header, payloads, suite, keys.SKei, keys.SKai, random)
|
||||
if encryptErr != nil {
|
||||
return ikeHeader{}, nil, encryptErr
|
||||
}
|
||||
outboundPackets = [][]byte{pkt}
|
||||
}
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
inboundPackets, err := transport.RoundTripExchange(ctx, outboundPackets)
|
||||
if err != nil {
|
||||
return ikeHeader{}, nil, err
|
||||
}
|
||||
return decryptAndValidateFragments(inboundPackets, header.InitiatorSPI, header.ResponderSPI, header.Exchange, header.MessageID, suite, keys)
|
||||
}
|
||||
|
||||
func hasNotifyType(payloads []payload, notifyType uint16) bool {
|
||||
for _, item := range payloadsOfType(payloads, payloadNotify) {
|
||||
kind, _, err := parseNotify(item)
|
||||
if err == nil && kind == notifyType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var errNoProposalChosen = errors.New("ike: responder reported NO_PROPOSAL_CHOSEN")
|
||||
|
||||
type invalidKEPayloadError struct {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (transport *firstAuthCaptureTransport) Float(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *firstAuthCaptureTransport) RoundTrip(_ context.Context, packet []byte) ([]byte, error) {
|
||||
func (transport *firstAuthCaptureTransport) RoundTrip(ctx context.Context, packet []byte) ([]byte, error) {
|
||||
transport.calls++
|
||||
if len(transport.cookieChallenge) > 0 {
|
||||
switch transport.calls {
|
||||
@@ -106,6 +106,17 @@ func (transport *firstAuthCaptureTransport) RoundTrip(_ context.Context, packet
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *firstAuthCaptureTransport) RoundTripExchange(ctx context.Context, packets [][]byte) ([][]byte, error) {
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("test: empty outbound packets")
|
||||
}
|
||||
resp, err := transport.RoundTrip(ctx, packets[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{resp}, nil
|
||||
}
|
||||
|
||||
func (transport *firstAuthCaptureTransport) answerIKECookie(packet []byte) ([]byte, error) {
|
||||
header, _, err := parseIKEPacket(packet)
|
||||
if err != nil {
|
||||
@@ -146,8 +157,8 @@ func (transport *firstAuthCaptureTransport) verifyIKECookie(packet []byte) error
|
||||
return errors.New("test: first retried IKE_SA_INIT payload is not the expected COOKIE")
|
||||
}
|
||||
cookies := payloadsOfType(payloads, payloadNotify)
|
||||
if len(cookies) != 3 {
|
||||
return fmt.Errorf("test: retried IKE_SA_INIT has %d notify payloads, want 3", len(cookies))
|
||||
if len(cookies) != 4 {
|
||||
return fmt.Errorf("test: retried IKE_SA_INIT has %d notify payloads, want 4", len(cookies))
|
||||
}
|
||||
found := false
|
||||
for _, item := range cookies {
|
||||
|
||||
@@ -39,7 +39,7 @@ func newSessionRelay(
|
||||
keepalive time.Duration,
|
||||
) *sessionRelay {
|
||||
if keepalive <= 0 {
|
||||
keepalive = 20 * time.Second
|
||||
keepalive = 15 * time.Second
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
relay := &sessionRelay{
|
||||
|
||||
@@ -3,6 +3,7 @@ package ike
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -49,6 +50,16 @@ func (*fakeSessionTransport) Float(context.Context) error { return nil }
|
||||
func (*fakeSessionTransport) RoundTrip(context.Context, []byte) ([]byte, error) {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
func (t *fakeSessionTransport) RoundTripExchange(ctx context.Context, packets [][]byte) ([][]byte, error) {
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("empty outbound packets")
|
||||
}
|
||||
resp, err := t.RoundTrip(ctx, packets[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return [][]byte{resp}, nil
|
||||
}
|
||||
func (transport *fakeSessionTransport) SendESP(ctx context.Context, packet []byte) error {
|
||||
return transport.SendSessionPacket(ctx, packet, false)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ type datagramTransport interface {
|
||||
RemoteAddr() *net.UDPAddr
|
||||
Float(context.Context) error
|
||||
RoundTrip(context.Context, []byte) ([]byte, error)
|
||||
RoundTripExchange(context.Context, [][]byte) ([][]byte, error)
|
||||
SendESP(context.Context, []byte) error
|
||||
ReceiveESP(context.Context, []byte) (int, error)
|
||||
SendSessionPacket(context.Context, []byte, bool) error
|
||||
@@ -96,6 +97,29 @@ func roundTripDatagram(
|
||||
read func([]byte, time.Time) (int, error),
|
||||
packet []byte,
|
||||
) ([]byte, error) {
|
||||
writeAll := func(values [][]byte) error {
|
||||
if len(values) > 0 {
|
||||
return write(values[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
responses, err := roundTripFragments(ctx, timeout, writeAll, read, [][]byte{packet})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, errors.New("ike: empty datagram response")
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func roundTripFragments(
|
||||
ctx context.Context,
|
||||
timeout time.Duration,
|
||||
writeAll func([][]byte) error,
|
||||
read func([]byte, time.Time) (int, error),
|
||||
packets [][]byte,
|
||||
) ([][]byte, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
@@ -110,20 +134,44 @@ func roundTripDatagram(
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := write(packet); err != nil {
|
||||
if err := writeAll(packets); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
attemptDeadline := time.Now().Add(interval)
|
||||
if deadline.Before(attemptDeadline) {
|
||||
attemptDeadline = deadline
|
||||
}
|
||||
var (
|
||||
totalExpected uint16
|
||||
fragments = make(map[uint16][]byte)
|
||||
)
|
||||
for time.Now().Before(attemptDeadline) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n, err := read(buffer, attemptDeadline)
|
||||
if err == nil {
|
||||
return append([]byte(nil), buffer[:n]...), nil
|
||||
pkt := append([]byte(nil), buffer[:n]...)
|
||||
header, body, parseErr := parseIKEPacket(pkt)
|
||||
if parseErr == nil && header.NextPayload == payloadEncryptedFragment && len(body) >= 8 {
|
||||
fragNum := binary.BigEndian.Uint16(body[4:6])
|
||||
total := binary.BigEndian.Uint16(body[6:8])
|
||||
if total > 1 {
|
||||
if totalExpected == 0 {
|
||||
totalExpected = total
|
||||
}
|
||||
fragments[fragNum] = pkt
|
||||
if uint16(len(fragments)) == totalExpected {
|
||||
res := make([][]byte, 0, totalExpected)
|
||||
for i := uint16(1); i <= totalExpected; i++ {
|
||||
res = append(res, fragments[i])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return [][]byte{pkt}, nil
|
||||
}
|
||||
if timeoutError, ok := err.(net.Error); ok && timeoutError.Timeout() {
|
||||
lastErr = err
|
||||
@@ -222,25 +270,47 @@ func (transport *directUDP) Float(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (transport *directUDP) RoundTrip(ctx context.Context, packet []byte) ([]byte, error) {
|
||||
responses, err := transport.RoundTripExchange(ctx, [][]byte{packet})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, errors.New("ike: empty exchange response")
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func (transport *directUDP) RoundTripExchange(ctx context.Context, packets [][]byte) ([][]byte, error) {
|
||||
transport.mu.Lock()
|
||||
defer transport.mu.Unlock()
|
||||
if transport.conn == nil {
|
||||
return nil, errors.New("ike: UDP transport is closed")
|
||||
}
|
||||
requestHeader, _, err := parseIKEPacket(packet)
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("ike: outbound packet list is empty")
|
||||
}
|
||||
requestHeader, _, err := parseIKEPacket(packets[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ike: invalid outbound packet: %w", err)
|
||||
}
|
||||
wirePacket := packet
|
||||
if transport.floated {
|
||||
wirePacket = append([]byte{0, 0, 0, 0}, packet...)
|
||||
}
|
||||
write := func(value []byte) error {
|
||||
if err := transport.conn.SetWriteDeadline(deadlineFor(ctx, transport.config.Timeout)); err != nil {
|
||||
return err
|
||||
var wirePackets [][]byte
|
||||
for _, pkt := range packets {
|
||||
wire := pkt
|
||||
if transport.floated {
|
||||
wire = append([]byte{0, 0, 0, 0}, pkt...)
|
||||
}
|
||||
_, err := transport.conn.Write(value)
|
||||
return err
|
||||
wirePackets = append(wirePackets, wire)
|
||||
}
|
||||
writeAll := func(values [][]byte) error {
|
||||
for _, value := range values {
|
||||
if err := transport.conn.SetWriteDeadline(deadlineFor(ctx, transport.config.Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := transport.conn.Write(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
read := func(buffer []byte, attemptDeadline time.Time) (int, error) {
|
||||
for {
|
||||
@@ -252,10 +322,6 @@ func (transport *directUDP) RoundTrip(ctx context.Context, packet []byte) ([]byt
|
||||
return 0, err
|
||||
}
|
||||
if transport.floated {
|
||||
// IKE and ESP legitimately share UDP/4500. An ESP packet can
|
||||
// arrive immediately before the IKE response that completes
|
||||
// CHILD_SA setup; discard it here and keep the same absolute
|
||||
// attempt deadline while waiting for marked IKE.
|
||||
if !hasNonESPMarker(buffer[:n]) {
|
||||
continue
|
||||
}
|
||||
@@ -268,7 +334,7 @@ func (transport *directUDP) RoundTrip(ctx context.Context, packet []byte) ([]byt
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return roundTripDatagram(ctx, transport.config.Timeout, write, read, wirePacket)
|
||||
return roundTripFragments(ctx, transport.config.Timeout, writeAll, read, wirePackets)
|
||||
}
|
||||
|
||||
func (transport *directUDP) SendESP(ctx context.Context, packet []byte) error {
|
||||
@@ -561,12 +627,26 @@ func (transport *socks5UDP) Float(_ context.Context) error {
|
||||
}
|
||||
|
||||
func (transport *socks5UDP) RoundTrip(ctx context.Context, packet []byte) ([]byte, error) {
|
||||
responses, err := transport.RoundTripExchange(ctx, [][]byte{packet})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
return nil, errors.New("ike: empty exchange response")
|
||||
}
|
||||
return responses[0], nil
|
||||
}
|
||||
|
||||
func (transport *socks5UDP) RoundTripExchange(ctx context.Context, packets [][]byte) ([][]byte, error) {
|
||||
transport.mu.Lock()
|
||||
defer transport.mu.Unlock()
|
||||
if transport.udp == nil {
|
||||
return nil, errors.New("ike: SOCKS5 UDP transport is closed")
|
||||
}
|
||||
requestHeader, _, err := parseIKEPacket(packet)
|
||||
if len(packets) == 0 {
|
||||
return nil, errors.New("ike: outbound packet list is empty")
|
||||
}
|
||||
requestHeader, _, err := parseIKEPacket(packets[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ike: invalid outbound packet: %w", err)
|
||||
}
|
||||
@@ -576,18 +656,18 @@ func (transport *socks5UDP) RoundTrip(ctx context.Context, packet []byte) ([]byt
|
||||
// Once a gateway answers, keep it pinned for the lifetime of the IKE SA.
|
||||
if !transport.floated && requestHeader.Exchange == exchangeIKEInit && requestHeader.MessageID == 0 && len(transport.remotes) > 1 {
|
||||
var lastErr error
|
||||
var cookieResponse []byte
|
||||
var cookieResponse [][]byte
|
||||
for _, candidate := range transport.remotes {
|
||||
transport.remote = cloneUDPAddr(candidate)
|
||||
response, attemptErr := transport.roundTripLocked(ctx, packet, requestHeader)
|
||||
responses, attemptErr := transport.roundTripFragmentsLocked(ctx, packets, requestHeader)
|
||||
if attemptErr == nil {
|
||||
if ikeInitResponseHasCookie(response) {
|
||||
if len(responses) > 0 && ikeInitResponseHasCookie(responses[0]) {
|
||||
if cookieResponse == nil {
|
||||
cookieResponse = append([]byte(nil), response...)
|
||||
cookieResponse = responses
|
||||
}
|
||||
continue
|
||||
}
|
||||
return response, nil
|
||||
return responses, nil
|
||||
}
|
||||
lastErr = attemptErr
|
||||
if ctx.Err() != nil || !isNetworkTimeout(attemptErr) {
|
||||
@@ -599,24 +679,32 @@ func (transport *socks5UDP) RoundTrip(ctx context.Context, packet []byte) ([]byt
|
||||
}
|
||||
return nil, fmt.Errorf("ike: all %d resolved ePDG addresses timed out: %w", len(transport.remotes), lastErr)
|
||||
}
|
||||
return transport.roundTripLocked(ctx, packet, requestHeader)
|
||||
return transport.roundTripFragmentsLocked(ctx, packets, requestHeader)
|
||||
}
|
||||
|
||||
func (transport *socks5UDP) roundTripLocked(ctx context.Context, packet []byte, requestHeader ikeHeader) ([]byte, error) {
|
||||
wireIKE := packet
|
||||
if transport.floated {
|
||||
wireIKE = append([]byte{0, 0, 0, 0}, packet...)
|
||||
}
|
||||
datagram, err := marshalSOCKS5Datagram(transport.remote, wireIKE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
write := func(value []byte) error {
|
||||
if err := transport.udp.SetWriteDeadline(deadlineFor(ctx, transport.config.Timeout)); err != nil {
|
||||
return err
|
||||
func (transport *socks5UDP) roundTripFragmentsLocked(ctx context.Context, packets [][]byte, requestHeader ikeHeader) ([][]byte, error) {
|
||||
var datagrams [][]byte
|
||||
for _, pkt := range packets {
|
||||
wireIKE := pkt
|
||||
if transport.floated {
|
||||
wireIKE = append([]byte{0, 0, 0, 0}, pkt...)
|
||||
}
|
||||
_, err := transport.udp.Write(value)
|
||||
return err
|
||||
datagram, err := marshalSOCKS5Datagram(transport.remote, wireIKE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
datagrams = append(datagrams, datagram)
|
||||
}
|
||||
writeAll := func(values [][]byte) error {
|
||||
for _, value := range values {
|
||||
if err := transport.udp.SetWriteDeadline(deadlineFor(ctx, transport.config.Timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := transport.udp.Write(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
read := func(buffer []byte, attemptDeadline time.Time) (int, error) {
|
||||
for {
|
||||
@@ -630,10 +718,6 @@ func (transport *socks5UDP) roundTripLocked(ctx context.Context, packet []byte,
|
||||
return 0, err
|
||||
}
|
||||
if transport.floated {
|
||||
// The relay can deliver ESP before the marked IKE response on
|
||||
// the same UDP/4500 association. Do not accept it as IKE, and
|
||||
// do not abort the exchange; keep waiting within the original
|
||||
// deadline.
|
||||
if !hasNonESPMarker(payload) {
|
||||
continue
|
||||
}
|
||||
@@ -646,7 +730,7 @@ func (transport *socks5UDP) roundTripLocked(ctx context.Context, packet []byte,
|
||||
return len(payload), nil
|
||||
}
|
||||
}
|
||||
return roundTripDatagram(ctx, transport.config.Timeout, write, read, datagram)
|
||||
return roundTripFragments(ctx, transport.config.Timeout, writeAll, read, datagrams)
|
||||
}
|
||||
|
||||
func isNetworkTimeout(err error) bool {
|
||||
|
||||
+14
-12
@@ -31,9 +31,10 @@ const (
|
||||
payloadDelete = 42
|
||||
payloadTSi = 44
|
||||
payloadTSr = 45
|
||||
payloadEncrypted = 46
|
||||
payloadCP = 47
|
||||
payloadEAP = 48
|
||||
payloadEncrypted = 46
|
||||
payloadCP = 47
|
||||
payloadEAP = 48
|
||||
payloadEncryptedFragment = 53
|
||||
|
||||
protocolIKE = 1
|
||||
protocolESP = 3
|
||||
@@ -55,15 +56,16 @@ const (
|
||||
dhMODP2048 = 14
|
||||
transformAttributeKeyLen = 14
|
||||
|
||||
notifyInitialContact = 16384
|
||||
notifyMOBIKESupported = 16396
|
||||
notifyNATSource = 16388
|
||||
notifyNATDestination = 16389
|
||||
notifyCookie = 16390
|
||||
notifyEAPOnlyAuth = 16417
|
||||
notifyDeviceIdentity = 41101
|
||||
notifyInvalidKE = 17
|
||||
notifyNoProposal = 14
|
||||
notifyInitialContact = 16384
|
||||
notifyMOBIKESupported = 16396
|
||||
notifyNATSource = 16388
|
||||
notifyNATDestination = 16389
|
||||
notifyCookie = 16390
|
||||
notifyEAPOnlyAuth = 16417
|
||||
notifyFragmentationSupported = 16430
|
||||
notifyDeviceIdentity = 41101
|
||||
notifyInvalidKE = 17
|
||||
notifyNoProposal = 14
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -56,7 +56,6 @@ type ReceivedSMS struct {
|
||||
RawTPDU string
|
||||
DecodeError string
|
||||
}
|
||||
|
||||
// ReceivedSMSStatus is network delivery evidence for one submitted SMS part.
|
||||
type ReceivedSMSStatus struct {
|
||||
DeviceID string
|
||||
|
||||
Reference in New Issue
Block a user