package netdetect_test import ( "reflect" "testing" "git.eeqj.de/sneak/rtnetmon/internal/netdetect" ) // Values reused across the selection tests. const ( osLinux = "linux" osDarwin = "darwin" ifaceGu0 = "gu0" ifaceBackhaul = "backhaul0" ifaceEth0 = "eth0" ifaceWlan0 = "wlan0" ifaceEn0 = "en0" ifaceUtun4 = "utun4" labelGu = "gu LAN - VPN outbound" labelCox = "Cox cable direct" labelDefault = "default route" addrEn0 = "192.168.1.20" ) // linuxFlags describes the Linux bridge interfaces rtnetmon was built for. func linuxFlags() netdetect.Flags { return netdetect.Flags{ IfaceA: ifaceGu0, LabelA: labelGu, IfaceB: ifaceBackhaul, LabelB: labelCox, } } // selectCase is one Select scenario with fake interfaces and routes. type selectCase struct { name string goos string ifaces []netdetect.Interface routes []netdetect.Route flags netdetect.Flags want []netdetect.Pane wantErr bool } // runSelectCases runs each case as a parallel subtest. func runSelectCases(t *testing.T, cases []selectCase) { t.Helper() for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := netdetect.Select(tt.goos, tt.ifaces, tt.routes, tt.flags) if tt.wantErr { if err == nil { t.Fatalf("Select() expected error, got panes %v", got) } return } if err != nil { t.Fatalf("Select() unexpected error: %v", err) } if !reflect.DeepEqual(got, tt.want) { t.Errorf("Select() = %v, want %v", got, tt.want) } }) } } func TestSelectLinuxPanes(t *testing.T) { t.Parallel() runSelectCases(t, []selectCase{ { name: "both bridge interfaces present", goos: osLinux, ifaces: []netdetect.Interface{ {Name: ifaceGu0, Up: true}, {Name: ifaceBackhaul, Up: true}, {Name: ifaceEth0, Up: true}, }, routes: []netdetect.Route{{Iface: ifaceEth0, Default: true}}, flags: linuxFlags(), want: []netdetect.Pane{ {Name: ifaceGu0, Label: labelGu}, {Name: ifaceBackhaul, Label: labelCox}, }, }, { name: "no bridge interfaces, single default route", goos: osLinux, ifaces: []netdetect.Interface{ {Name: ifaceEth0, Up: true}, {Name: "lo", Up: true}, }, routes: []netdetect.Route{{Iface: ifaceEth0, Default: true}}, flags: linuxFlags(), want: []netdetect.Pane{{Name: ifaceEth0, Label: labelDefault}}, }, { name: "single default route keeps explicit label", goos: osLinux, ifaces: []netdetect.Interface{{Name: ifaceWlan0, Up: true}}, routes: []netdetect.Route{{Iface: ifaceWlan0, Default: true}}, flags: netdetect.Flags{ IfaceA: ifaceGu0, LabelA: "Home WiFi", LabelASet: true, IfaceB: ifaceBackhaul, LabelB: labelCox, }, want: []netdetect.Pane{{Name: ifaceWlan0, Label: "Home WiFi"}}, }, }) } func TestSelectLinuxErrors(t *testing.T) { t.Parallel() runSelectCases(t, []selectCase{ { name: "only one bridge interface present", goos: osLinux, ifaces: []netdetect.Interface{ {Name: ifaceGu0, Up: true}, {Name: ifaceEth0, Up: true}, }, routes: []netdetect.Route{{Iface: ifaceEth0, Default: true}}, flags: linuxFlags(), wantErr: true, }, { name: "no bridge, no default route", goos: osLinux, ifaces: []netdetect.Interface{{Name: ifaceEth0, Up: true}}, routes: nil, flags: linuxFlags(), wantErr: true, }, { name: "no bridge, multiple default routes", goos: osLinux, ifaces: []netdetect.Interface{ {Name: ifaceEth0, Up: true}, {Name: "eth1", Up: true}, }, routes: []netdetect.Route{ {Iface: ifaceEth0, Default: true}, {Iface: "eth1", Default: true}, }, flags: linuxFlags(), wantErr: true, }, }) } func TestSelectDarwinPanes(t *testing.T) { t.Parallel() runSelectCases(t, []selectCase{ { name: "vpn tunnel plus physical default route", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, {Name: ifaceUtun4, Up: true, IPv4: []string{"10.64.0.2"}}, {Name: "utun0", Up: true, IPv4: nil}, }, routes: []netdetect.Route{ {Iface: ifaceUtun4, Default: true}, {Iface: ifaceEn0, Default: true}, }, flags: linuxFlags(), want: []netdetect.Pane{ {Name: ifaceUtun4, Label: "VPN"}, {Name: ifaceEn0, Label: labelDefault}, }, }, { name: "vpn detected by routable address without its own route", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, {Name: "utun6", Up: true, IPv4: []string{"10.2.0.2"}}, }, routes: []netdetect.Route{{Iface: ifaceEn0, Default: true}}, flags: linuxFlags(), want: []netdetect.Pane{ {Name: "utun6", Label: "VPN"}, {Name: ifaceEn0, Label: labelDefault}, }, }, { name: "vpn pane honors explicit labels", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, {Name: ifaceUtun4, Up: true, IPv4: []string{"10.64.0.2"}}, }, routes: []netdetect.Route{ {Iface: ifaceUtun4, Default: true}, {Iface: ifaceEn0, Default: true}, }, flags: netdetect.Flags{ LabelA: "Mullvad", LabelASet: true, LabelB: "Fiber", LabelBSet: true, }, want: []netdetect.Pane{ {Name: ifaceUtun4, Label: "Mullvad"}, {Name: ifaceEn0, Label: "Fiber"}, }, }, }) } func TestSelectDarwinSingleAndErrors(t *testing.T) { t.Parallel() runSelectCases(t, []selectCase{ { name: "no vpn, single physical default route", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, {Name: "utun0", Up: true, IPv4: nil}, {Name: "utun1", Up: true, IPv4: []string{"169.254.1.1"}}, }, routes: []netdetect.Route{{Iface: ifaceEn0, Default: true}}, flags: linuxFlags(), want: []netdetect.Pane{{Name: ifaceEn0, Label: labelDefault}}, }, { name: "no default route", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, }, routes: nil, flags: linuxFlags(), wantErr: true, }, { name: "two physical default routes", goos: osDarwin, ifaces: []netdetect.Interface{ {Name: ifaceEn0, Up: true, IPv4: []string{addrEn0}}, {Name: "en1", Up: true, IPv4: []string{"192.168.2.20"}}, }, routes: []netdetect.Route{ {Iface: ifaceEn0, Default: true}, {Iface: "en1", Default: true}, }, flags: linuxFlags(), wantErr: true, }, { name: "unsupported operating system", goos: "windows", wantErr: true, }, }) } func TestParseIPRoute(t *testing.T) { t.Parallel() out := "default via 192.168.1.1 dev eth0 proto dhcp metric 100\n" got := netdetect.ParseIPRoute(out) want := []netdetect.Route{{Iface: ifaceEth0, Gateway: "192.168.1.1", Default: true}} if !reflect.DeepEqual(got, want) { t.Errorf("ParseIPRoute() = %v, want %v", got, want) } } func TestParseProcNetRoute(t *testing.T) { t.Parallel() out := "Iface\tDestination\tGateway\tFlags\tRefCnt\tUse\tMetric\tMask\n" + "eth0\t00000000\t0102A8C0\t0003\t0\t0\t100\t00000000\n" + "eth0\t0002A8C0\t00000000\t0001\t0\t0\t0\t00FFFFFF\n" got := netdetect.ParseProcNetRoute(out) want := []netdetect.Route{{Iface: ifaceEth0, Gateway: "192.168.2.1", Default: true}} if !reflect.DeepEqual(got, want) { t.Errorf("ParseProcNetRoute() = %v, want %v", got, want) } } func TestParseNetstat(t *testing.T) { t.Parallel() out := "Routing tables\n\nInternet:\n" + "Destination Gateway Flags Netif Expire\n" + "default 10.0.0.1 UGScg en0\n" + "default link#15 UCSg utun4\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}, } if !reflect.DeepEqual(got, want) { t.Errorf("ParseNetstat() = %v, want %v", got, want) } }