Skip to content

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

Description

@polaris-arch

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions