Detect interfaces per platform; add macOS and single-interface support (closes #2)
check / check (push) Failing after 1s
check / check (push) Failing after 1s
Linux runs exactly as before when both bridge interfaces exist. When they do not, the lone default-route interface is monitored, and a single interface now draws a single UI pane instead of an empty second one. macOS is newly supported: a running VPN tunnel (utun) is monitored as the primary pane alongside the physical default-route interface, or the physical interface alone when no VPN is up. Detection lives in internal/netdetect: interface/route data types, pure selection logic keyed on OS name, and route parsers, all unit-tested on Linux for both platforms. Only the real route query and the per-platform TCP dial binding (source address on Linux, IP_BOUND_IF on macOS) are build-tagged. Ping argument construction is a pure, OS-keyed function. NewMonitor now takes a list of interfaces. Model: opus-4-8
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user