@@ -340,7 +340,12 @@ type TunnelManager struct {
340340 unsupportedDevices map [string ]bool
341341 // lastProbe tracks when each tunnel was last liveness-probed so probes run
342342 // at most once per probeInterval.
343- lastProbe map [string ]time.Time
343+ lastProbe map [string ]time.Time
344+ // probeFailures counts consecutive failed liveness probes per udid. A tunnel
345+ // is only torn down after probeFailureThreshold consecutive failures so a
346+ // single transient probe error (a momentary route hiccup or load spike)
347+ // doesn't destroy an otherwise healthy tunnel. A successful probe resets it.
348+ probeFailures map [string ]int
344349 probeInterval time.Duration
345350 startTunnelTimeout time.Duration
346351 firstUpdateCompleted bool
@@ -387,6 +392,7 @@ func newTunnelManager(pm PairRecordManager, userspaceTUN bool, udidFilter string
387392 failedDevices : map [string ]failedDevice {},
388393 unsupportedDevices : map [string ]bool {},
389394 lastProbe : map [string ]time.Time {},
395+ probeFailures : map [string ]int {},
390396 probeInterval : defaultProbeInterval ,
391397 startTunnelTimeout : 10 * time .Second ,
392398 userspaceTUN : userspaceTUN ,
@@ -460,19 +466,18 @@ func (m *TunnelManager) UpdateTunnels(ctx context.Context) error {
460466 // Liveness-probe existing tunnel records of still-connected devices: a
461467 // tunnel can die without a usbmux disconnect (quick reboot, transport
462468 // death), leaving a stale record that would otherwise be served forever. A
463- // failed probe tears the record down here so the create loop below rebuilds
464- // it in the same cycle. Records of vanished devices are handled by the
465- // disconnect teardown at the end.
469+ // tunnel that fails probeFailureThreshold consecutive probes is torn down
470+ // here so the create loop below rebuilds it in the same cycle. Records of
471+ // vanished devices are handled by the disconnect teardown at the end.
472+ //
473+ // Probes run concurrently (bounded) because a dead-but-still-routed tunnel
474+ // blackholes the SYN and burns the full probeDialTimeout; probing serially
475+ // would let N dead tunnels delay tunnel creation for newly connected devices
476+ // by up to probeDialTimeout*N.
466477 now := time .Now ()
467- for udid , tun := range localTunnels {
468- if ! currentUDIDs [udid ] || ! m .shouldProbe (udid , now ) {
469- continue
470- }
471- if err := m .pr .Probe (tun ); err != nil {
472- golog .Warn ("tunnel failed liveness probe, restarting it" , "module" , logModule , "udid" , udid , "error" , err )
473- _ = m .stopTunnel (tun )
474- delete (localTunnels , udid )
475- }
478+ for udid := range m .probeTunnels (localTunnels , currentUDIDs , now ) {
479+ _ = m .stopTunnel (localTunnels [udid ])
480+ delete (localTunnels , udid )
476481 }
477482
478483 for _ , d := range devices .DeviceList {
@@ -541,6 +546,21 @@ func (m *TunnelManager) UpdateTunnels(ctx context.Context) error {
541546 delete (m .failedDevices , udid )
542547 }
543548 }
549+ // Prune per-device bookkeeping for devices that are no longer connected so
550+ // these maps stay bounded by the current device count instead of growing
551+ // with every udid ever seen. Pruning unsupportedDevices on disconnect also
552+ // lets a device that was unsupported, then upgraded to a tunnel-capable iOS
553+ // and reconnected, be retried instead of skipped for the rest of the process.
554+ for udid := range m .unsupportedDevices {
555+ if ! currentUDIDs [udid ] {
556+ delete (m .unsupportedDevices , udid )
557+ }
558+ }
559+ for udid := range m .probeFailures {
560+ if ! currentUDIDs [udid ] {
561+ delete (m .probeFailures , udid )
562+ }
563+ }
544564 m .firstUpdateCompleted = true
545565 m .mux .Unlock ()
546566 return nil
@@ -574,6 +594,61 @@ func (m *TunnelManager) shouldProbe(udid string, now time.Time) bool {
574594 return true
575595}
576596
597+ // probeTunnels liveness-probes every due tunnel of a still-connected device and
598+ // returns the set of udids that should be torn down. Probes run concurrently
599+ // with at most maxConcurrentProbes in flight so a batch of dead-but-routed
600+ // tunnels (each of which blackholes the SYN for the full probeDialTimeout)
601+ // can't serialize into a multi-second stall of the update loop. A tunnel is only
602+ // reported dead after probeFailureThreshold consecutive failures; a success
603+ // resets its counter, so a single transient probe error is absorbed.
604+ func (m * TunnelManager ) probeTunnels (localTunnels map [string ]Tunnel , currentUDIDs map [string ]bool , now time.Time ) map [string ]bool {
605+ type probeResult struct {
606+ udid string
607+ err error
608+ }
609+ results := make (chan probeResult )
610+ sem := make (chan struct {}, maxConcurrentProbes )
611+ var wg sync.WaitGroup
612+ for udid , tun := range localTunnels {
613+ if ! currentUDIDs [udid ] || ! m .shouldProbe (udid , now ) {
614+ continue
615+ }
616+ wg .Add (1 )
617+ go func (udid string , tun Tunnel ) {
618+ defer wg .Done ()
619+ sem <- struct {}{}
620+ defer func () { <- sem }()
621+ results <- probeResult {udid : udid , err : m .pr .Probe (tun )}
622+ }(udid , tun )
623+ }
624+ go func () {
625+ wg .Wait ()
626+ close (results )
627+ }()
628+
629+ dead := map [string ]bool {}
630+ m .mux .Lock ()
631+ defer m .mux .Unlock ()
632+ for r := range results {
633+ if r .err == nil {
634+ delete (m .probeFailures , r .udid )
635+ continue
636+ }
637+ m .probeFailures [r .udid ]++
638+ fails := m .probeFailures [r .udid ]
639+ if fails < probeFailureThreshold {
640+ golog .Warn ("tunnel failed liveness probe, will retry before restarting it" ,
641+ "module" , logModule , "udid" , r .udid , "consecutiveFailures" , fails , "threshold" , probeFailureThreshold , "error" , r .err )
642+ continue
643+ }
644+ golog .Warn ("tunnel failed liveness probe repeatedly, restarting it" ,
645+ "module" , logModule , "udid" , r .udid , "consecutiveFailures" , fails , "error" , r .err )
646+ delete (m .probeFailures , r .udid )
647+ dead [r .udid ] = true
648+ }
649+ return dead
650+ }
651+
577652// failedDeviceBackoff returns how long to wait before retrying a device after
578653// failCount consecutive failures: 30s, 60s, 120s, 240s, capped at 5 minutes.
579654func failedDeviceBackoff (failCount int ) time.Duration {
@@ -598,6 +673,7 @@ func (m *TunnelManager) RemoveTunnel(ctx context.Context, serialNumber string) e
598673 delete (m .tunnels , serialNumber )
599674 }
600675 delete (m .lastProbe , serialNumber )
676+ delete (m .probeFailures , serialNumber )
601677 m .mux .Unlock ()
602678
603679 if ! exists {
@@ -612,6 +688,7 @@ func (m *TunnelManager) stopTunnel(t Tunnel) error {
612688 golog .Info ("stopping tunnel" , "module" , logModule , "udid" , t .Udid )
613689 delete (m .tunnels , t .Udid )
614690 delete (m .lastProbe , t .Udid )
691+ delete (m .probeFailures , t .Udid )
615692 m .mux .Unlock ()
616693
617694 return t .Close ()
@@ -673,15 +750,29 @@ type tunnelProber interface {
673750}
674751
675752// defaultProbeInterval is how often the TunnelManager liveness-probes each
676- // tunnel record. Combined with the probe's short dial timeout, a dead tunnel
677- // is detected and rebuilt well within a minute instead of never.
753+ // tunnel record. Combined with the probe's short dial timeout and the
754+ // probeFailureThreshold consecutive-failure requirement, a persistently dead
755+ // tunnel is detected and rebuilt within roughly a minute instead of never,
756+ // while a single transient probe error never tears down a healthy tunnel.
678757const defaultProbeInterval = 30 * time .Second
679758
680759// probeDialTimeout bounds the liveness probe's TCP connect. A healthy tunnel
681760// answers in milliseconds; a dead-but-still-routed one swallows the SYN, so a
682761// short timeout is enough to tell them apart without stalling UpdateTunnels.
683762const probeDialTimeout = 5 * time .Second
684763
764+ // maxConcurrentProbes caps how many liveness probes run at once. Probes are
765+ // concurrent so a batch of dead-but-routed tunnels can't serialize into a
766+ // probeDialTimeout*N stall, but bounded so a large fleet can't open an
767+ // unbounded number of dial sockets in one cycle.
768+ const maxConcurrentProbes = 8
769+
770+ // probeFailureThreshold is the number of consecutive failed liveness probes
771+ // required before a tunnel is torn down and rebuilt. Requiring two failures
772+ // keeps a single transient probe error (a momentary route hiccup) from
773+ // destroying a healthy tunnel.
774+ const probeFailureThreshold = 2
775+
685776// rsdProber is the default tunnelProber: a short-timeout TCP dial of the
686777// tunnel's RSD endpoint over the kernel TUN route.
687778type rsdProber struct {
0 commit comments