macOS: ignore on-link default routes when picking the physical uplink #18

Merged
sneak merged 1 commits from macos-onlink-default-route into next 2026-11-07 05:23:31 +01:00
Collaborator

Problem

On a Mac running a VM with NAT networking (vmnet / Internet Sharing), rtnetmon refuses to start:

Error: multiple physical default-route interfaces found; rtnetmon supports only one (bridge100, en0)

netstat -rn -f inet has three default rows:

default            10.100.205.254     UGScg                 en0
default            link#31            UCSIg               utun6
default            link#45            UCSIg           bridge100      !

The utun6 row is a tunnel and already ignored. The bridge100 row is installed by vmnet for the VM subnet. It is interface-scoped (I) and has no next-hop gateway: the gateway column is the link itself (link#45, no G flag). Nothing beyond the bridge's own subnet is reachable through it, so it is not an uplink.

Scoped default routes cannot simply be dropped: while a VPN holds the default route, the physical interface keeps only a default route scoped to itself (default 192.168.1.1 UGScIg en0), and that is how the physical pane is found in the VPN case.

Fix

A physical default route on macOS now requires a next-hop gateway (flag G). The physical interface's default always has one, scoped or not; the on-link pseudo-defaults from vmnet and Internet Sharing never do. parseNetstat records a gateway only when G is set.

Verification

  • make test, make fmt, make lint clean.
  • New TestSelectDarwinVMBridge covers the bridge row next to the physical default, next to a connected VPN, and alone (error).
  • Built and run on the affected Mac: detection now logs Monitoring 1 interface(s) and picks en0.
  • README detection rules updated.

No tracked issue: issue creation was not permitted from the session that made this change.

## Problem On a Mac running a VM with NAT networking (vmnet / Internet Sharing), `rtnetmon` refuses to start: ``` Error: multiple physical default-route interfaces found; rtnetmon supports only one (bridge100, en0) ``` `netstat -rn -f inet` has three `default` rows: ``` default 10.100.205.254 UGScg en0 default link#31 UCSIg utun6 default link#45 UCSIg bridge100 ! ``` The `utun6` row is a tunnel and already ignored. The `bridge100` row is installed by vmnet for the VM subnet. It is interface-scoped (`I`) and has no next-hop gateway: the gateway column is the link itself (`link#45`, no `G` flag). Nothing beyond the bridge's own subnet is reachable through it, so it is not an uplink. Scoped default routes cannot simply be dropped: while a VPN holds the default route, the physical interface keeps only a default route scoped to itself (`default 192.168.1.1 UGScIg en0`), and that is how the physical pane is found in the VPN case. ## Fix A physical default route on macOS now requires a next-hop gateway (flag `G`). The physical interface's default always has one, scoped or not; the on-link pseudo-defaults from vmnet and Internet Sharing never do. `parseNetstat` records a gateway only when `G` is set. ## Verification - `make test`, `make fmt`, `make lint` clean. - New `TestSelectDarwinVMBridge` covers the bridge row next to the physical default, next to a connected VPN, and alone (error). - Built and run on the affected Mac: detection now logs `Monitoring 1 interface(s)` and picks `en0`. - README detection rules updated. No tracked issue: issue creation was not permitted from the session that made this change.
clawbot added 1 commit 2026-11-07 04:08:25 +01:00
A VM with NAT networking makes vmnet install a default route scoped to
its bridge (bridge100) with no gateway. The detector counted it as a
second physical default route and refused to start:

    multiple physical default-route interfaces found (bridge100, en0)

Scoped routes cannot simply be dropped: while a VPN holds the default
route, the physical interface keeps only a default route scoped to
itself. That route still names its gateway (flag G). The on-link one
does not, so a physical default route now requires a next-hop gateway.

parseNetstat records a gateway only when the G flag is set; a link#N
gateway column is the interface itself, not a next hop.
sneak merged commit 053c00cd52 into next 2026-11-07 05:23:31 +01:00
sneak deleted branch macos-onlink-default-route 2026-11-07 05:23:32 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/rtnetmon#18