SagerNet / SagerNet/sing-tun

macOS: setRoutes EEXIST-recovery deletes the system default route (literal 0.0.0.0/0); unsetRoutes never restores it

Open
#80 0 comments 0 reactions 0 assignees View on GitHub

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: true plus a system: true wireguard endpoint whose peer has allowed_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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.