macOS: setRoutes EEXIST-recovery deletes the system default route (literal 0.0.0.0/0); unsetRoutes never restores it
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 237
- Forks
- 228
- PR merge metrics
- No merged PRs in 30d
Description
Summary
On macOS, NativeTun.setRoutes()'s EEXIST recovery deletes the host's pre-existing global default route whenever a route range is a literal 0.0.0.0/0, and unsetRoutes() never restores it. Observed via a sing-box system: true WireGuard endpoint carrying full-tunnel: the machine loses its default route on start, and after the tunnel stops it has no default route → no internet until the network is re-kicked manually (e.g. toggling Wi‑Fi).
Environment
- sing-tun (current
main), via sing-box 1.14.0-alpha - macOS 26.x, Apple Silicon (arm64)
- Repro config (sing-box): a TUN inbound with
auto_route: trueplus asystem: truewireguard endpoint whose peer hasallowed_ips: ["0.0.0.0/0", "::/0"].
Root cause — tun_darwin.go
setRoutes() installs each range from BuildAutoRouteRanges() via execRoute(RTM_ADD, …). With auto_route, 0.0.0.0/0 is split into reserved-excluding sub-ranges, so a literal 0.0.0.0/0 is never installed. But the system WireGuard interface installs its allowed_ips verbatim, so the range list contains a literal 0.0.0.0/0.
// setRoutes(), ~L448
err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, interfaceIndex, destination, gateway)
if err != nil {
if errors.Is(err, unix.EEXIST) {
err = execRoute(unix.RTM_DELETE, false, 0, destination, gateway) // deletes the EXISTING 0.0.0.0/0 = the system default
...
err = execRoute(unix.RTM_ADD, t.options.InterfaceScope, ...) // re-add as ifscope
RTM_ADD 0.0.0.0/0 returns EEXIST (the system's global default already exists); the recovery RTM_DELETE 0.0.0.0/0 removes the system's global default (macOS matches by destination), then re-adds the WG's as interface-scoped. On teardown, unsetRoutes() (~L471-493) deletes the endpoint's own ranges but never restores the default it displaced.
Evidence (route -n monitor, single sing-box pid)
Start:
RTM_ADD default <wg-gateway> ifscope <utunN>
RTM_DELETE default <en0-gateway> (GLOBAL, no ifscope) <-- system default deleted
Stop: only RTM_DELETE of the endpoint's own ranges; no re-add of the original default. netstat -rn -f inet afterwards has no default entry -> no connectivity.
Why the auto_route TUN does not hit this
The auto_route TUN goes through BuildAutoRouteRanges(), which splits 0.0.0.0/0 into reserved-excluding sub-ranges — it never installs a literal 0.0.0.0/0, so RTM_ADD never collides with the system default and the EEXIST branch is never taken for 0/0.
Expected
The EEXIST recovery should not delete (or should restore) the system's primary default route. Options: split 0.0.0.0/0 for system interfaces the same way auto_route does; or have setRoutes() remember any pre-existing default it displaced and unsetRoutes() restore it.
Workaround (downstream)
Capture the default gateway before start and restore it via route add -inet default <gw> after stop, deriving the current gateway from scutil State:/Network/Global/IPv4 so it survives mid-session network changes.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in tun_darwin.go around setRoutes() and unsetRoutes() (~L448 and ~L471-493), then reproduce the route changes with route -n monitor and netstat -rn -f inet. Trace the EEXIST recovery for a literal 0.0.0.0/0 and define the fix so the pre-existing global default remains available after the tunnel stops.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, macos
- Domain
- networking, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100