Repository navigation
fix(darwin): survive Mac sleep — configd routing, zombie relay, libusb self-heal, MAC adoption - #12
Open
catalinb13 wants to merge 5 commits into
Open
catalinb13 wants to merge 5 commits into
catalinb13 wants to merge 5 commits into
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 andmDNSRespondersuppressed it.getaddrinfofailed 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 (-99forever, 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 activerndis0MAC 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 ./...andgo 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.netstat -rn -f inetshows the split0/1+128.0/1routes through the tunnel; DNS resolves via the tunnel; throughput measured ~67 Mbps.Notes
daemon.go,relay.go) — expect a conflict between the two; the root-cause chain here is the more complete one for the post-sleep zombie case.