mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
Enhance session relaying to handle delayed IKE packets and implement an automatic retry mechanism
This commit is contained in:
@@ -87,6 +87,13 @@ func (relay *sessionRelay) run() {
|
||||
packet := append([]byte(nil), buffer[:n]...)
|
||||
if isIKE {
|
||||
if err := relay.handleIKE(packet); err != nil {
|
||||
if errors.Is(err, errMismatchedSessionSPIs) {
|
||||
// A reconnect can reuse the same NAT mapping while the ePDG still
|
||||
// has packets queued for the previous IKE SA. Those packets are
|
||||
// unrelated to this authenticated session and must be discarded;
|
||||
// treating one as fatal tears down the newly established CHILD_SA.
|
||||
continue
|
||||
}
|
||||
relay.fail(err)
|
||||
return
|
||||
}
|
||||
@@ -111,13 +118,15 @@ func (relay *sessionRelay) run() {
|
||||
}
|
||||
}
|
||||
|
||||
var errMismatchedSessionSPIs = errors.New("ike: session packet has mismatched SPIs")
|
||||
|
||||
func (relay *sessionRelay) handleIKE(packet []byte) error {
|
||||
header, _, err := parseIKEPacket(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if header.InitiatorSPI != relay.spii || header.ResponderSPI != relay.spir {
|
||||
return errors.New("ike: session packet has mismatched SPIs")
|
||||
return errMismatchedSessionSPIs
|
||||
}
|
||||
if header.Flags&flagResponse != 0 {
|
||||
return nil
|
||||
|
||||
@@ -178,3 +178,39 @@ func TestSessionRelaySendsNATKeepalive(t *testing.T) {
|
||||
t.Fatal("relay did not send a NAT-T keepalive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionRelayDropsDelayedIKEPacketFromPreviousSA(t *testing.T) {
|
||||
transport := newFakeSessionTransport()
|
||||
spii := [8]byte{1}
|
||||
spir := [8]byte{2}
|
||||
relay := newSessionRelay(
|
||||
transport,
|
||||
legacyTestSuite(),
|
||||
ikeKeys{},
|
||||
spii,
|
||||
spir,
|
||||
true,
|
||||
time.Hour,
|
||||
)
|
||||
defer relay.Close()
|
||||
|
||||
transport.incoming <- fakeSessionPacket{
|
||||
ike: true,
|
||||
data: ikeHeader{
|
||||
InitiatorSPI: [8]byte{9},
|
||||
ResponderSPI: [8]byte{8},
|
||||
Exchange: exchangeInformational,
|
||||
}.marshal(nil),
|
||||
}
|
||||
wantedESP := []byte{0, 0, 0, 9, 0, 0, 0, 1, 0xaa}
|
||||
transport.incoming <- fakeSessionPacket{data: wantedESP}
|
||||
|
||||
buffer := make([]byte, 64)
|
||||
count, err := relay.ReceiveESP(context.Background(), buffer)
|
||||
if err != nil {
|
||||
t.Fatalf("ReceiveESP() after stale IKE packet = %v", err)
|
||||
}
|
||||
if !bytes.Equal(buffer[:count], wantedESP) {
|
||||
t.Fatalf("ESP after stale IKE packet = %x, want %x", buffer[:count], wantedESP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ var (
|
||||
ErrClosed = errors.New("vowifi runtime: manager is closed")
|
||||
)
|
||||
|
||||
const defaultOperationTimeout = 2 * time.Minute
|
||||
const (
|
||||
defaultOperationTimeout = 2 * time.Minute
|
||||
defaultRetryInitial = 2 * time.Second
|
||||
defaultRetryMaximum = 30 * time.Second
|
||||
)
|
||||
|
||||
type StateHandler func(context.Context, vowifi.State) error
|
||||
type OrchestratorFactory func(context.Context, string) (*vowifi.Orchestrator, error)
|
||||
@@ -28,6 +32,8 @@ type OrchestratorFactory func(context.Context, string) (*vowifi.Orchestrator, er
|
||||
type Options struct {
|
||||
Logger *slog.Logger
|
||||
OperationTimeout time.Duration
|
||||
RetryInitial time.Duration
|
||||
RetryMaximum time.Duration
|
||||
OnState StateHandler
|
||||
Factory OrchestratorFactory
|
||||
}
|
||||
@@ -37,6 +43,8 @@ type Manager struct {
|
||||
cancel context.CancelFunc
|
||||
logger *slog.Logger
|
||||
operationTimeout time.Duration
|
||||
retryInitial time.Duration
|
||||
retryMaximum time.Duration
|
||||
onState StateHandler
|
||||
factory OrchestratorFactory
|
||||
|
||||
@@ -50,6 +58,11 @@ type entry struct {
|
||||
orchestrator *vowifi.Orchestrator
|
||||
busy bool
|
||||
reconnectPending bool
|
||||
disablePending bool
|
||||
desiredEnabled bool
|
||||
autoRetryPending bool
|
||||
retryFailures uint
|
||||
operationCancel context.CancelFunc
|
||||
stopWatch func()
|
||||
}
|
||||
|
||||
@@ -60,12 +73,23 @@ func New(options Options) *Manager {
|
||||
if options.OperationTimeout <= 0 {
|
||||
options.OperationTimeout = defaultOperationTimeout
|
||||
}
|
||||
if options.RetryInitial <= 0 {
|
||||
options.RetryInitial = defaultRetryInitial
|
||||
}
|
||||
if options.RetryMaximum <= 0 {
|
||||
options.RetryMaximum = defaultRetryMaximum
|
||||
}
|
||||
if options.RetryMaximum < options.RetryInitial {
|
||||
options.RetryMaximum = options.RetryInitial
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Manager{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
logger: options.Logger,
|
||||
operationTimeout: options.OperationTimeout,
|
||||
retryInitial: options.RetryInitial,
|
||||
retryMaximum: options.RetryMaximum,
|
||||
onState: options.OnState,
|
||||
factory: options.Factory,
|
||||
entries: make(map[string]*entry),
|
||||
@@ -184,6 +208,20 @@ func (manager *Manager) RequestEnabled(deviceID string, enabled bool) (vowifi.St
|
||||
if err := manager.Ensure(manager.ctx, deviceID); err != nil {
|
||||
return vowifi.State{}, err
|
||||
}
|
||||
manager.mu.Lock()
|
||||
item := manager.entries[deviceID]
|
||||
item.desiredEnabled = enabled
|
||||
if !enabled && item.busy {
|
||||
item.disablePending = true
|
||||
cancel := item.operationCancel
|
||||
state := item.orchestrator.State()
|
||||
manager.mu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
return manager.startOperation(deviceID, false, func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
if enabled {
|
||||
_, err := orchestrator.Enable(ctx)
|
||||
@@ -198,6 +236,11 @@ func (manager *Manager) RequestReconnect(deviceID string) (vowifi.State, error)
|
||||
if err := manager.Ensure(manager.ctx, deviceID); err != nil {
|
||||
return vowifi.State{}, err
|
||||
}
|
||||
manager.mu.Lock()
|
||||
if item := manager.entries[deviceID]; item != nil {
|
||||
item.desiredEnabled = true
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
return manager.startOperation(deviceID, true, func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Reconnect(ctx)
|
||||
return err
|
||||
@@ -270,6 +313,17 @@ func (manager *Manager) runOperations(
|
||||
defer manager.wg.Done()
|
||||
for {
|
||||
ctx, cancel := context.WithTimeout(manager.ctx, manager.operationTimeout)
|
||||
manager.mu.Lock()
|
||||
if item.disablePending {
|
||||
item.disablePending = false
|
||||
item.reconnectPending = false
|
||||
operation = func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Disable(ctx)
|
||||
return err
|
||||
}
|
||||
}
|
||||
item.operationCancel = cancel
|
||||
manager.mu.Unlock()
|
||||
err := operation(ctx, item.orchestrator)
|
||||
cancel()
|
||||
if err != nil &&
|
||||
@@ -281,23 +335,114 @@ func (manager *Manager) runOperations(
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
state := item.orchestrator.State()
|
||||
manager.mu.Lock()
|
||||
if manager.closed || !item.reconnectPending {
|
||||
item.operationCancel = nil
|
||||
if manager.closed {
|
||||
item.busy = false
|
||||
manager.mu.Unlock()
|
||||
return
|
||||
}
|
||||
item.reconnectPending = false
|
||||
if item.disablePending {
|
||||
item.disablePending = false
|
||||
item.reconnectPending = false
|
||||
manager.mu.Unlock()
|
||||
operation = func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Disable(ctx)
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if item.reconnectPending {
|
||||
item.reconnectPending = false
|
||||
manager.mu.Unlock()
|
||||
|
||||
// Read the route only when this runs. If the user bound, unbound,
|
||||
// then rebound while busy, this reconnect uses the final persisted
|
||||
// binding instead of replaying stale intermediate routes.
|
||||
operation = func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Reconnect(ctx)
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
item.busy = false
|
||||
shouldRetry := item.desiredEnabled && state.Phase == vowifi.PhaseFailed
|
||||
if !shouldRetry && state.Phase != vowifi.PhaseFailed {
|
||||
item.retryFailures = 0
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
if shouldRetry {
|
||||
manager.scheduleAutoRetry(deviceID, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *Manager) scheduleAutoRetry(deviceID string, item *entry) {
|
||||
manager.mu.Lock()
|
||||
if manager.closed || item.busy || item.autoRetryPending || !item.desiredEnabled {
|
||||
manager.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delay := manager.retryInitial
|
||||
for attempt := uint(0); attempt < item.retryFailures && delay < manager.retryMaximum; attempt++ {
|
||||
if delay > manager.retryMaximum/2 {
|
||||
delay = manager.retryMaximum
|
||||
break
|
||||
}
|
||||
delay *= 2
|
||||
}
|
||||
if delay > manager.retryMaximum {
|
||||
delay = manager.retryMaximum
|
||||
}
|
||||
item.retryFailures++
|
||||
item.autoRetryPending = true
|
||||
manager.wg.Add(1)
|
||||
manager.mu.Unlock()
|
||||
|
||||
manager.logger.Info(
|
||||
"VoWiFi automatic retry scheduled",
|
||||
"device_id", deviceID,
|
||||
"retry_in", delay,
|
||||
)
|
||||
go func() {
|
||||
defer manager.wg.Done()
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-manager.ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
manager.mu.Lock()
|
||||
item.autoRetryPending = false
|
||||
if manager.closed || manager.entries[deviceID] != item || !item.desiredEnabled {
|
||||
manager.mu.Unlock()
|
||||
return
|
||||
}
|
||||
state := item.orchestrator.State()
|
||||
if state.Phase != vowifi.PhaseFailed {
|
||||
if state.Phase != vowifi.PhaseStopping {
|
||||
item.retryFailures = 0
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if item.busy {
|
||||
manager.mu.Unlock()
|
||||
return
|
||||
}
|
||||
item.busy = true
|
||||
manager.wg.Add(1)
|
||||
manager.mu.Unlock()
|
||||
|
||||
// Read the route only when this runs. If the user bound, unbound, then
|
||||
// rebound while busy, the single reconnect uses the final persisted
|
||||
// binding instead of replaying stale intermediate routes.
|
||||
operation = func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Reconnect(ctx)
|
||||
go manager.runOperations(deviceID, item, func(ctx context.Context, orchestrator *vowifi.Orchestrator) error {
|
||||
_, err := orchestrator.Retry(ctx)
|
||||
return err
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (manager *Manager) watch(deviceID string, states <-chan vowifi.State) {
|
||||
@@ -310,6 +455,20 @@ func (manager *Manager) watch(deviceID string, states <-chan vowifi.State) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if state.Phase == vowifi.PhaseFailed {
|
||||
manager.mu.Lock()
|
||||
item := manager.entries[deviceID]
|
||||
manager.mu.Unlock()
|
||||
if item != nil {
|
||||
manager.scheduleAutoRetry(deviceID, item)
|
||||
}
|
||||
} else if state.Phase == vowifi.PhaseSMSReady || !state.Enabled {
|
||||
manager.mu.Lock()
|
||||
if item := manager.entries[deviceID]; item != nil {
|
||||
item.retryFailures = 0
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
}
|
||||
if manager.onState == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -63,6 +63,31 @@ func (fakeTunnelSession) Evidence() vowifi.TunnelEvidence {
|
||||
}
|
||||
func (fakeTunnelSession) Close(context.Context) error { return nil }
|
||||
|
||||
type flakyTunnelProvider struct {
|
||||
mu sync.Mutex
|
||||
attempts int
|
||||
failures int
|
||||
}
|
||||
|
||||
func (provider *flakyTunnelProvider) Start(
|
||||
context.Context,
|
||||
vowifi.TunnelRequest,
|
||||
) (vowifi.TunnelSession, error) {
|
||||
provider.mu.Lock()
|
||||
defer provider.mu.Unlock()
|
||||
provider.attempts++
|
||||
if provider.attempts <= provider.failures {
|
||||
return nil, errors.New("temporary tunnel failure")
|
||||
}
|
||||
return fakeTunnelSession{}, nil
|
||||
}
|
||||
|
||||
func (provider *flakyTunnelProvider) Attempts() int {
|
||||
provider.mu.Lock()
|
||||
defer provider.mu.Unlock()
|
||||
return provider.attempts
|
||||
}
|
||||
|
||||
type fakeIMSProvider struct{}
|
||||
type fakeIMSSession struct{}
|
||||
|
||||
@@ -104,6 +129,27 @@ func testOrchestrator(t *testing.T, id string) *vowifi.Orchestrator {
|
||||
return orchestrator
|
||||
}
|
||||
|
||||
func testOrchestratorWithTunnel(
|
||||
t *testing.T,
|
||||
id string,
|
||||
tunnel vowifi.TunnelProvider,
|
||||
) *vowifi.Orchestrator {
|
||||
t.Helper()
|
||||
orchestrator, err := vowifi.New(vowifi.Dependencies{
|
||||
SIM: fakeSIM{},
|
||||
AKA: fakeAKA{},
|
||||
Radio: fakeRadio{},
|
||||
Proxy: fakeProxy{},
|
||||
Tunnel: tunnel,
|
||||
IMS: fakeIMSProvider{},
|
||||
Phones: fakePhones{},
|
||||
}, vowifi.Options{DeviceID: id})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return orchestrator
|
||||
}
|
||||
|
||||
func TestManagerRunsAndPublishesEnable(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var states []vowifi.State
|
||||
@@ -148,6 +194,79 @@ func TestManagerRunsAndPublishesEnable(t *testing.T) {
|
||||
t.Fatal("enable did not finish")
|
||||
}
|
||||
|
||||
func TestManagerRetriesEnabledPolicyUntilReady(t *testing.T) {
|
||||
provider := &flakyTunnelProvider{failures: 2}
|
||||
manager := New(Options{
|
||||
OperationTimeout: time.Second,
|
||||
RetryInitial: 5 * time.Millisecond,
|
||||
RetryMaximum: 10 * time.Millisecond,
|
||||
})
|
||||
t.Cleanup(func() { _ = manager.Close(context.Background()) })
|
||||
if err := manager.Register(testOrchestratorWithTunnel(t, "ec20", provider)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manager.RequestEnabled("ec20", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
state, err := manager.State("ec20")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Phase == vowifi.PhaseSMSReady {
|
||||
if attempts := provider.Attempts(); attempts != 3 {
|
||||
t.Fatalf("tunnel attempts = %d, want 3", attempts)
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
t.Fatalf("VoWiFi did not become ready after retries; attempts=%d", provider.Attempts())
|
||||
}
|
||||
|
||||
func TestManagerStopsAutomaticRetryWhenPolicyIsDisabled(t *testing.T) {
|
||||
provider := &flakyTunnelProvider{failures: 100}
|
||||
manager := New(Options{
|
||||
OperationTimeout: time.Second,
|
||||
RetryInitial: 100 * time.Millisecond,
|
||||
RetryMaximum: 100 * time.Millisecond,
|
||||
})
|
||||
t.Cleanup(func() { _ = manager.Close(context.Background()) })
|
||||
if err := manager.Register(testOrchestratorWithTunnel(t, "ec20", provider)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := manager.RequestEnabled("ec20", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
manager.mu.Lock()
|
||||
pending := manager.entries["ec20"].autoRetryPending
|
||||
manager.mu.Unlock()
|
||||
if pending {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if _, err := manager.RequestEnabled("ec20", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if attempts := provider.Attempts(); attempts != 1 {
|
||||
t.Fatalf("tunnel attempts after disable = %d, want 1", attempts)
|
||||
}
|
||||
state, err := manager.State("ec20")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if state.Enabled || state.Phase != vowifi.PhaseIdle {
|
||||
t.Fatalf("state after disabling retry policy = %+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerRejectsUnknownDevice(t *testing.T) {
|
||||
manager := New(Options{})
|
||||
t.Cleanup(func() {
|
||||
|
||||
Reference in New Issue
Block a user