macOS: setRoutes EEXIST-recovery deletes the system default route (literal 0.0.0.0/0); unsetRoutes never restores it
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- go, macos
- Domain
- networking, operating-systems
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.
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.
- Dominant language
- Go
- Stars
- 240
- Forks
- 240
- PR merge metrics
- No merged PRs in 30d
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
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.
More from SagerNet/sing-tun
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
gvisor: LinkEndpointFilter.Attach never forwards nil, so GVisor.Close leaks all inbound dispatcher goroutines and fdsPossibly taken @arpitjain099 claimed this 47 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Darwin: utun process UUID headers are parsed as IP payloadPossibly taken @d3pre5s claimed this today. Open
Difficulty 4/5 3-5 days Newbie friendliness 25/100
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
-
TUN `mixed` stack: TCP never re-injected into tun0 on Linux kernel 7.1 (UDP works, `gvisor` works)Open
Difficulty 4/5 1-2 days Newbie friendliness 45/100
All issues in SagerNet/sing-tun
Similar issues
-
[BUG]: Windows GUI package of v1.17.0-rc3 has no GTK DLLsPossibly taken @andrut-ds claimed this today. Openbug CI/CD Pactus-GUI Windows
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
pactus-project/pactus#2428 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 91/100
Maintainers usually reply within 1 day
-
feat
Difficulty 1/5 Under an hour Newbie friendliness 65/100
ory/hydra-maester#312 · 1 reaction ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
rossoctl/context-guru#610 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 1 day