Skip to content

fix(darwin): survive Mac sleep — configd routing, zombie relay, libusb self-heal, MAC adoption - #12

Open
catalinb13 wants to merge 5 commits into
HelloPrincePal:mainfrom
catalinb13:fix/darwin-post-sleep-recovery
Open

catalinb13 wants to merge 5 commits into
HelloPrincePal:mainfrom
catalinb13:fix/darwin-post-sleep-recovery

Conversation

@catalinb13

Copy link
Copy Markdown

Summary

Five commits that fix the macOS post-sleep failure chain on this project: after Mac sleep the tunnel looks connected but every unicast is silently dropped (DNS fails, ping fails), and the daemon's libusb context can poison itself to -99 (permanently deaf) until a full restart.

Tested for several days on a Pixel 3 XL + macOS (arm64). This exact combination used to be the classic "zombie tunnel after sleep" state; after these fixes the tunnel is the live link with working DNS and ~67 Mbps throughput.

What's in each commit

  • e6c5cbd — relay loop guard: the relay reflected phone→Mac frames back at the phone, and MAC changes were adopted on a single transient packet. Loops dropped; MAC adoption now requires a threshold.
  • adad122 — configd registration (fixes 🐛 [Bug]: getaddrinfo fails while tethered when no Wi-Fi service is active (scutil DNS entry is Supplemental-only) #11): the tunnel was not registered as a routable configd service, so its DNS entry was Supplemental-only and mDNSResponder suppressed it. getaddrinfo failed while the tunnel was up whenever no Wi-Fi service was active.
  • 27cd380 — libusb self-heal: a 500 ms re-attach storm after a Mac sleep permanently poisoned the libusb context (-99 forever, watcher deaf). The watcher now exits after 10 consecutive scan errors so launchd restarts it cleanly, with handshake backoff and a user notification.
  • 2c7ffb6 — zombie session killer: a link watchdog (10 s gateway ping, 3 misses) tears down half-dead sessions where the phone left tethering but the tunnel stayed up.
  • dbba472 — the real root cause: the RNDIS handshake reports the gadget's permanent MAC, but Android re-randomizes the active rndis0 MAC on every tethering enable. DHCP (broadcast) worked while unicast was silently dropped. The relay now adopts the source MAC of any UDP 67→68 (DHCP) frame immediately.

Verification

  • go build ./... and go test ./... pass; three new test files (relay_loopguard_test.go, watchdog_verify_test.go, watchdog_live_test.go) cover the loop guard and watchdog behavior.
  • Live: netstat -rn -f inet shows the split 0/1 + 128.0/1 routes through the tunnel; DNS resolves via the tunnel; throughput measured ~67 Mbps.

Notes

PC and others added 5 commits September 27, 2026 18:42
…reshold

Frames returning to the Mac carrying our own client MAC (02:00:...:01) are
a phone-side loop (USB + hotspot both attached); forwarding them into utun
poisoned the path and mis-set the phone MAC. Drop them, counting drops.
A new phone MAC is adopted only after macAdoptThreshold (8) consecutive
sightings from the same source, so single stray frames cannot mis-address
every subsequent frame.
…nder suppression

macOS 26+ regression: DNS set via scutil alone yields a SUPPLEMENTAL-only
resolver. Without the RequestA bit (0x2), mDNSResponder marks A/AAAA records
'unusable' and suppresses every getaddrinfo query — Chromium/Electron apps
keep working (own resolver) while Firefox/brew/curl fail on a tether that
routes fine.

Fix (proven end-to-end on macOS 27.0): make droidtether a real
SystemConfiguration service.
- tun: RegisterService() writes State:/Network/Service/droidtether/IPv4
  {InterfaceName, Addresses, SubnetMasks /24, Router}; configd builds the
  route list itself, the service becomes routable, configd's own primary
  election sets Global/IPv4 PrimaryService=droidtether and the derived
  resolver gets RequestA (store __FLAGS__ 16386 + __CONFIGURATION_ID__).
- tun: SetDNS keeps __FLAGS__ # 6 (CFNumber; '*' makes an array configd
  ignores) on the service dict and now REMOVES any raw
  State:/Network/Global/DNS instead of setting it: raw Global/DNS writes
  never reach mDNSResponder and make configd tear down the election
  (A/B probes). Close() removes the service IPv4+DNS dicts.
- daemon: config watchdog (interval via route.watchdog_interval_s, 10s
  default) re-applies drift after standby: interface IP, split /1 routes,
  and — checked via the consumer's view (scutil --dns resolver HelloPrincePal#1 must
  carry 8.8.8.8 + 'Request A records', plus PrimaryService election), not
  the misleading raw store flags — service registration + DNS.
- daemon: serialize device sessions, always release claimed USB interfaces
  on exit, bind watchdog to a session context so it never stacks or
  outlives the relay.

Verified: getaddrinfo ~60ms, brew update / curl / dscacheutil green,
Firefox confirmed, only harmless AAAA suppressions (no IPv6 on utun),
/etc/hosts workarounds removed.
…dshake backoff

After a Mac sleep Android disables USB tethering itself; the phone still
enumerates the RNDIS config but the gadget never answers INIT. The old
daemon re-attached every 500ms against that dead/surprise-removed device,
which poisoned the libusb context permanently (libusb_get_device_list
returns LIBUSB_ERROR_OTHER -99 forever) and the daemon went deaf even
after the user re-enabled tethering.

- watcher: track consecutive scan errors; exit(1) at 10 so launchd
  KeepAlive restarts the daemon with a fresh libusb context.
- daemon: linear backoff (2s..30s cap) on consecutive handshake failures,
  resets on success; after 5 failures notify the console user via
  launchctl asuser + osascript to re-enable USB tethering on the phone.
  (Android blocks adb from enabling tethering itself: SELinux shell
  context, 'cmd tethering' absent, service call tethering 1 ->
  SecurityException.)
Android disables USB tethering when the phone screen locks: the phone
re-enumerates without the RNDIS config, but the relay's writes to the
dead handle keep 'succeeding' (gousb does not error), so the session
becomes a zombie that holds the watcher's active-device slot — the
watcher then refuses to re-attach even after the user re-enables
tethering, and the daemon just sits there incrementing a sent counter
against a device that is not there.

Probe the phone gateway through the tunnel every 10s; after 3
consecutive failed probes end the session so the watcher can re-attach
(or wait for the user's re-enable, covered by the handshake-failure
notification).
…reshold

The RNDIS handshake reports the gadget's *permanent* MAC, but Android
re-randomizes the active rndis0 MAC per tethering enable. The relay's
8-consecutive-packet adoption threshold never sees enough DHCP frames
before the link watchdog kills the session, so every unicast frame
(ping, DNS) was addressed to the stale MAC and silently dropped by the
phone — DHCP (broadcast) still worked, which is exactly the observed
'DHCP ok, ping fails' signature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 [Bug]: getaddrinfo fails while tethered when no Wi-Fi service is active (scutil DNS entry is Supplemental-only)

1 participant