macOS: ignore on-link default routes when picking the physical uplink
check / check (push) Successful in 2m32s
check / check (push) Successful in 2m32s
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.
This commit is contained in:
1 parent
7686440108
commit
6dc3b9e395
3 files changed
+138
-23
No files matched your search
@@ -17,6 +17,7 @@ const (
|
||||
ifaceEth0 = "eth0"
|
||||
ifaceWlan0 = "wlan0"
|
||||
ifaceEn0 = "en0"
|
||||
ifaceBridge = "bridge100"
|
||||
ifaceUtun0 = "utun0"
|
||||
ifaceUtun3 = "utun3"
|
||||
ifaceUtun4 = "utun4"
|
||||
@@ -27,6 +28,8 @@ const (
|
||||
labelVPN = "VPN"
|
||||
|
||||
addrEn0 = "192.168.1.20"
|
||||
gatewayEn0 = "192.168.1.1"
|
||||
addrBridge = "192.168.64.1"
|
||||
addrVPN = "10.64.0.2"
|
||||
addrTailscale = "100.101.102.103"
|
||||
)
|
||||
@@ -106,6 +109,32 @@ default 192.168.1.1 UGScg en0
|
||||
100.101.102.103/32 link#22 UCS utun3
|
||||
127.0.0.1 127.0.0.1 UH lo0
|
||||
128.0/1 utun4 USc utun4
|
||||
`
|
||||
|
||||
// A VM with NAT networking: vmnet installs an on-link default route,
|
||||
// scoped to its bridge and with no gateway, next to the real one.
|
||||
netstatVMBridge = netstatHeader + `
|
||||
default 192.168.1.1 UGScg en0
|
||||
default link#45 UCSIg bridge100 !
|
||||
127.0.0.1 127.0.0.1 UH lo0
|
||||
192.168.64 link#45 UC bridge100 !
|
||||
`
|
||||
|
||||
// The same VM bridge while a VPN holds the default route.
|
||||
netstatVMBridgeAndVPN = netstatHeader + `
|
||||
default link#15 UCSg utun4
|
||||
default 192.168.1.1 UGScIg en0
|
||||
default link#45 UCSIg bridge100 !
|
||||
10.64.0.2 10.64.0.2 UH utun4
|
||||
127.0.0.1 127.0.0.1 UH lo0
|
||||
192.168.64 link#45 UC bridge100 !
|
||||
`
|
||||
|
||||
// Only the VM bridge's on-link default route, no uplink at all.
|
||||
netstatVMBridgeOnly = netstatHeader + `
|
||||
default link#45 UCSIg bridge100 !
|
||||
127.0.0.1 127.0.0.1 UH lo0
|
||||
192.168.64 link#45 UC bridge100 !
|
||||
`
|
||||
)
|
||||
|
||||
@@ -265,7 +294,7 @@ func TestSelectDarwinPanes(t *testing.T) {
|
||||
},
|
||||
routes: []netdetect.Route{
|
||||
{Iface: ifaceUtun4, Default: true},
|
||||
{Iface: ifaceEn0, Default: true},
|
||||
{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true, Scoped: true},
|
||||
},
|
||||
flags: linuxFlags(),
|
||||
want: []netdetect.Pane{
|
||||
@@ -282,7 +311,7 @@ func TestSelectDarwinPanes(t *testing.T) {
|
||||
},
|
||||
routes: []netdetect.Route{
|
||||
{Iface: ifaceUtun4, Default: true},
|
||||
{Iface: ifaceEn0, Default: true},
|
||||
{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true, Scoped: true},
|
||||
},
|
||||
flags: netdetect.Flags{
|
||||
LabelA: "Mullvad", LabelASet: true,
|
||||
@@ -308,7 +337,7 @@ func TestSelectDarwinSingleAndErrors(t *testing.T) {
|
||||
{Name: ifaceUtun0, Up: true, IPv4: nil},
|
||||
{Name: "utun1", Up: true, IPv4: []string{"169.254.1.1"}},
|
||||
},
|
||||
routes: []netdetect.Route{{Iface: ifaceEn0, Default: true}},
|
||||
routes: []netdetect.Route{{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true}},
|
||||
flags: linuxFlags(),
|
||||
want: []netdetect.Pane{{Name: ifaceEn0, Label: labelDefault}},
|
||||
},
|
||||
@@ -319,7 +348,7 @@ func TestSelectDarwinSingleAndErrors(t *testing.T) {
|
||||
{Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}},
|
||||
{Name: "utun6", Up: true, IPv4: []string{"10.2.0.2"}},
|
||||
},
|
||||
routes: []netdetect.Route{{Iface: ifaceEn0, Default: true}},
|
||||
routes: []netdetect.Route{{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true}},
|
||||
flags: linuxFlags(),
|
||||
want: []netdetect.Pane{{Name: ifaceEn0, Label: labelDefault}},
|
||||
},
|
||||
@@ -341,8 +370,8 @@ func TestSelectDarwinSingleAndErrors(t *testing.T) {
|
||||
{Name: "en1", Up: true, IPv4: []string{"192.168.2.20"}},
|
||||
},
|
||||
routes: []netdetect.Route{
|
||||
{Iface: ifaceEn0, Default: true},
|
||||
{Iface: "en1", Default: true},
|
||||
{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true},
|
||||
{Iface: "en1", Gateway: "192.168.2.1", Default: true},
|
||||
},
|
||||
flags: linuxFlags(),
|
||||
wantErr: true,
|
||||
@@ -430,6 +459,63 @@ func TestSelectDarwinFromNetstat(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestSelectDarwinVMBridge covers a VM's NAT bridge: vmnet installs a default
|
||||
// route scoped to the bridge with no gateway, which is not an uplink.
|
||||
func TestSelectDarwinVMBridge(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
vpn := netdetect.Interface{Name: ifaceUtun4, Up: true, IPv4: []string{addrVPN}}
|
||||
bridge := netdetect.Interface{Name: ifaceBridge, Up: true, IPv4: []string{addrBridge}}
|
||||
physicalOnly := []netdetect.Pane{{Name: ifaceEn0, Label: labelDefault}}
|
||||
vpnAndPhysical := []netdetect.Pane{
|
||||
{Name: ifaceUtun4, Label: labelVPN},
|
||||
{Name: ifaceEn0, Label: labelDefault},
|
||||
}
|
||||
|
||||
runSelectCases(t, []selectCase{
|
||||
{
|
||||
name: "on-link default route is not an uplink",
|
||||
goos: osDarwin,
|
||||
ifaces: macIfaces(bridge),
|
||||
routes: []netdetect.Route{
|
||||
{Iface: ifaceEn0, Gateway: gatewayEn0, Default: true},
|
||||
{Iface: ifaceBridge, Default: true, Scoped: true},
|
||||
},
|
||||
flags: linuxFlags(),
|
||||
want: physicalOnly,
|
||||
},
|
||||
{
|
||||
name: "only an on-link default route",
|
||||
goos: osDarwin,
|
||||
ifaces: macIfaces(bridge),
|
||||
routes: []netdetect.Route{{Iface: ifaceBridge, Default: true, Scoped: true}},
|
||||
flags: linuxFlags(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "netstat: vm bridge next to the physical default",
|
||||
goos: osDarwin,
|
||||
ifaces: macIfaces(bridge),
|
||||
routes: netdetect.ParseNetstat(netstatVMBridge),
|
||||
want: physicalOnly,
|
||||
},
|
||||
{
|
||||
name: "netstat: vm bridge next to a connected vpn",
|
||||
goos: osDarwin,
|
||||
ifaces: macIfaces(bridge, vpn),
|
||||
routes: netdetect.ParseNetstat(netstatVMBridgeAndVPN),
|
||||
want: vpnAndPhysical,
|
||||
},
|
||||
{
|
||||
name: "netstat: vm bridge with no uplink",
|
||||
goos: osDarwin,
|
||||
ifaces: macIfaces(bridge),
|
||||
routes: netdetect.ParseNetstat(netstatVMBridgeOnly),
|
||||
wantErr: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseIPRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -463,11 +549,13 @@ func TestParseNetstat(t *testing.T) {
|
||||
"Destination Gateway Flags Netif Expire\n" +
|
||||
"default 10.0.0.1 UGScg en0\n" +
|
||||
"default link#15 UCSg utun4\n" +
|
||||
"default link#45 UCSIg bridge100 !\n" +
|
||||
"127.0.0.1 127.0.0.1 UH lo0\n"
|
||||
got := netdetect.ParseNetstat(out)
|
||||
want := []netdetect.Route{
|
||||
{Iface: ifaceEn0, Gateway: "10.0.0.1", Default: true},
|
||||
{Iface: ifaceUtun4, Gateway: "link#15", Default: true},
|
||||
{Iface: ifaceUtun4, Default: true},
|
||||
{Iface: ifaceBridge, Default: true, Scoped: true},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
|
||||
Reference in new issue
Block a user