mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
490 lines
16 KiB
Go
490 lines
16 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
"tailscale.com/net/netmon"
|
|
)
|
|
|
|
// deltaTestInterface describes one interface of a test network state.
|
|
type deltaTestInterface struct {
|
|
name string
|
|
up bool
|
|
mac string
|
|
mtu int
|
|
ips []string
|
|
}
|
|
|
|
var (
|
|
deltaTestUpFlags = net.FlagUp | net.FlagBroadcast | net.FlagMulticast
|
|
deltaTestDownFlags = net.Flags(0)
|
|
)
|
|
|
|
func deltaTestState(t *testing.T, route string, ifaces ...deltaTestInterface) *netmon.State {
|
|
t.Helper()
|
|
state := &netmon.State{
|
|
DefaultRouteInterface: route,
|
|
Interface: map[string]netmon.Interface{},
|
|
InterfaceIPs: map[string][]netip.Prefix{},
|
|
}
|
|
for _, iface := range ifaces {
|
|
flags := deltaTestDownFlags
|
|
if iface.up {
|
|
flags = deltaTestUpFlags
|
|
}
|
|
var mac net.HardwareAddr
|
|
if iface.mac != "" {
|
|
parsed, err := net.ParseMAC(iface.mac)
|
|
if err != nil {
|
|
t.Fatalf("ParseMAC(%q) error: %v", iface.mac, err)
|
|
}
|
|
mac = parsed
|
|
}
|
|
state.Interface[iface.name] = netmon.Interface{Interface: &net.Interface{
|
|
Name: iface.name,
|
|
Flags: flags,
|
|
MTU: iface.mtu,
|
|
HardwareAddr: mac,
|
|
}}
|
|
for _, ip := range iface.ips {
|
|
state.InterfaceIPs[iface.name] = append(state.InterfaceIPs[iface.name], netip.MustParsePrefix(ip))
|
|
}
|
|
}
|
|
return state
|
|
}
|
|
|
|
func sameInterfaceChange(got, want interfaceChange) bool {
|
|
return got.Name == want.Name &&
|
|
got.Action == want.Action &&
|
|
got.Class == want.Class &&
|
|
got.HardwarePort == want.HardwarePort &&
|
|
got.Service == want.Service &&
|
|
slices.Equal(got.IPsBefore, want.IPsBefore) &&
|
|
slices.Equal(got.IPsAfter, want.IPsAfter) &&
|
|
got.Flags == want.Flags &&
|
|
got.MTU == want.MTU &&
|
|
got.IsDefaultRoute == want.IsDefaultRoute
|
|
}
|
|
|
|
func assertInterfaceChanges(t *testing.T, got, want []interfaceChange) {
|
|
t.Helper()
|
|
if len(got) != len(want) {
|
|
t.Fatalf("changes = %+v, want %+v", got, want)
|
|
}
|
|
for i := range want {
|
|
if !sameInterfaceChange(got[i], want[i]) {
|
|
t.Errorf("change %d = %+v, want %+v", i, got[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func Test_diffNetworkDelta(t *testing.T) {
|
|
en0 := deltaTestInterface{name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1500, ips: []string{"192.0.2.10/24"}}
|
|
awdl0 := deltaTestInterface{name: "awdl0", up: true, mac: "aa:bb:cc:dd:ee:02", mtu: 1484, ips: []string{"fe80::1/64"}}
|
|
awdl0NewMAC := deltaTestInterface{name: "awdl0", up: true, mac: "aa:bb:cc:dd:ee:03", mtu: 1484, ips: []string{"fe80::1/64"}}
|
|
en5 := deltaTestInterface{name: "en5", up: true, mac: "aa:bb:cc:dd:ee:05", mtu: 1500, ips: []string{"192.0.2.50/24"}}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
routeOld string
|
|
routeNew string
|
|
nilBefore bool
|
|
before []deltaTestInterface
|
|
after []deltaTestInterface
|
|
meta interfaceMetaFunc
|
|
want []interfaceChange
|
|
}{
|
|
{
|
|
name: "airdrop_mac_change_with_link_local_only",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0NewMAC},
|
|
want: []interfaceChange{{
|
|
Name: "awdl0", Action: "mac_changed", Class: "airdrop",
|
|
IPsBefore: []string{"fe80::1/64"}, IPsAfter: []string{"fe80::1/64"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1484,
|
|
}},
|
|
},
|
|
{
|
|
name: "airdrop_mac_and_link_local_change",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, {
|
|
name: "awdl0", up: true, mac: "aa:bb:cc:dd:ee:03", mtu: 1484, ips: []string{"fe80::2/64"},
|
|
}},
|
|
want: []interfaceChange{{
|
|
Name: "awdl0", Action: "ip_removed", Class: "airdrop",
|
|
IPsBefore: []string{"fe80::1/64"}, IPsAfter: []string{"fe80::2/64"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1484,
|
|
}},
|
|
},
|
|
{
|
|
name: "hardware_address_added_with_airdrop_change",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{{
|
|
name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1500,
|
|
ips: []string{"192.0.2.10/24", "192.0.2.11/24"},
|
|
}, awdl0NewMAC},
|
|
want: []interfaceChange{
|
|
{
|
|
Name: "awdl0", Action: "mac_changed", Class: "airdrop",
|
|
IPsBefore: []string{"fe80::1/64"}, IPsAfter: []string{"fe80::1/64"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1484,
|
|
},
|
|
{
|
|
Name: "en0", Action: "ip_added", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24", "192.0.2.11/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "interface_removed",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, en5},
|
|
after: []deltaTestInterface{en0},
|
|
want: []interfaceChange{{
|
|
Name: "en5", Action: "removed", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.50/24"}, IPsAfter: nil,
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500,
|
|
}},
|
|
},
|
|
{
|
|
name: "interface_added",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0},
|
|
after: []deltaTestInterface{en0, en5},
|
|
want: []interfaceChange{{
|
|
Name: "en5", Action: "added", Class: "hardware",
|
|
IPsBefore: nil, IPsAfter: []string{"192.0.2.50/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500,
|
|
}},
|
|
},
|
|
{
|
|
name: "address_removed",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{{
|
|
name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1500,
|
|
ips: []string{"192.0.2.10/24", "192.0.2.11/24"},
|
|
}},
|
|
after: []deltaTestInterface{en0},
|
|
want: []interfaceChange{{
|
|
Name: "en0", Action: "ip_removed", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24", "192.0.2.11/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "interface_down",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0},
|
|
after: []deltaTestInterface{{
|
|
name: "en0", up: false, mac: "aa:bb:cc:dd:ee:01", mtu: 1500, ips: []string{"192.0.2.10/24"},
|
|
}},
|
|
want: []interfaceChange{{
|
|
Name: "en0", Action: "down", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestDownFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "interface_up",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{{
|
|
name: "en0", up: false, mac: "aa:bb:cc:dd:ee:01", mtu: 1500, ips: []string{"192.0.2.10/24"},
|
|
}},
|
|
after: []deltaTestInterface{en0},
|
|
want: []interfaceChange{{
|
|
Name: "en0", Action: "up", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "mtu_changed",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0},
|
|
after: []deltaTestInterface{{
|
|
name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1400, ips: []string{"192.0.2.10/24"},
|
|
}},
|
|
want: []interfaceChange{{
|
|
Name: "en0", Action: "mtu_changed", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1400, IsDefaultRoute: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "default_route_moves_to_the_new_interface",
|
|
routeOld: "en0", routeNew: "en5",
|
|
before: []deltaTestInterface{en0},
|
|
after: []deltaTestInterface{{
|
|
name: "en0", up: false, mac: "aa:bb:cc:dd:ee:01", mtu: 1500, ips: []string{"192.0.2.10/24"},
|
|
}, en5},
|
|
want: []interfaceChange{
|
|
{
|
|
Name: "en0", Action: "down", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestDownFlags.String(), MTU: 1500,
|
|
},
|
|
{
|
|
Name: "en5", Action: "added", Class: "hardware",
|
|
IPsBefore: nil, IPsAfter: []string{"192.0.2.50/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "nil_old_state_adds_every_interface",
|
|
routeNew: "en0",
|
|
nilBefore: true,
|
|
after: []deltaTestInterface{en0},
|
|
want: []interfaceChange{{
|
|
Name: "en0", Action: "added", Class: "hardware",
|
|
IPsBefore: nil, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1500, IsDefaultRoute: true,
|
|
}},
|
|
},
|
|
{
|
|
name: "no_change",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "meta_fills_the_platform_fields",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, {name: "utun3", up: true, mtu: 1400}},
|
|
after: []deltaTestInterface{{
|
|
name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1400, ips: []string{"192.0.2.10/24"},
|
|
}, {name: "utun3", up: true, mtu: 1380}},
|
|
meta: func(name string) interfaceMeta {
|
|
if name == "utun3" {
|
|
return interfaceMeta{Class: "virtual"}
|
|
}
|
|
return interfaceMeta{HardwarePort: "Wi-Fi", Service: "Wi-Fi"}
|
|
},
|
|
want: []interfaceChange{
|
|
{
|
|
Name: "en0", Action: "mtu_changed", Class: "hardware",
|
|
HardwarePort: "Wi-Fi", Service: "Wi-Fi",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1400, IsDefaultRoute: true,
|
|
},
|
|
{
|
|
Name: "utun3", Action: "mtu_changed", Class: "virtual",
|
|
Flags: deltaTestUpFlags.String(), MTU: 1380,
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var before *netmon.State
|
|
if !tt.nilBefore {
|
|
before = deltaTestState(t, tt.routeOld, tt.before...)
|
|
}
|
|
changes := diffNetworkDelta(before, deltaTestState(t, tt.routeNew, tt.after...))
|
|
describeInterfaceChanges(changes, tt.meta)
|
|
assertInterfaceChanges(t, changes, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_diffNetworkDeltaVirtualSet(t *testing.T) {
|
|
virtual := virtualInterfaceSetFn
|
|
t.Cleanup(func() { virtualInterfaceSetFn = virtual })
|
|
virtualInterfaceSetFn = func() map[string]struct{} { return map[string]struct{}{"docker0": {}} }
|
|
|
|
before := deltaTestState(t, "en0",
|
|
deltaTestInterface{name: "en0", up: true, mtu: 1500, ips: []string{"192.0.2.10/24"}},
|
|
deltaTestInterface{name: "docker0", up: true, mtu: 1500},
|
|
)
|
|
after := deltaTestState(t, "en0",
|
|
deltaTestInterface{name: "en0", up: true, mtu: 1400, ips: []string{"192.0.2.10/24"}},
|
|
deltaTestInterface{name: "docker0", up: true, mtu: 1400},
|
|
)
|
|
assertInterfaceChanges(t, diffNetworkDelta(before, after), []interfaceChange{
|
|
{
|
|
Name: "docker0", Action: "mtu_changed", Class: "virtual",
|
|
Flags: deltaTestUpFlags.String(), MTU: 1400,
|
|
},
|
|
{
|
|
Name: "en0", Action: "mtu_changed", Class: "hardware",
|
|
IPsBefore: []string{"192.0.2.10/24"}, IPsAfter: []string{"192.0.2.10/24"},
|
|
Flags: deltaTestUpFlags.String(), MTU: 1400, IsDefaultRoute: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
func Test_interfaceClass(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
virtual bool
|
|
want string
|
|
}{
|
|
{name: "lo0", want: "loopback"},
|
|
{name: "awdl0", want: "airdrop"},
|
|
{name: "llw0", want: "airdrop"},
|
|
{name: "utun3", want: "tunnel"},
|
|
{name: "tun0", want: "tunnel"},
|
|
{name: "tap0", want: "tunnel"},
|
|
{name: "wg0", want: "tunnel"},
|
|
{name: "ipsec0", want: "tunnel"},
|
|
{name: "ppp0", want: "tunnel"},
|
|
{name: "docker0", virtual: true, want: "virtual"},
|
|
{name: "docker0", want: "hardware"},
|
|
{name: "en0", want: "hardware"},
|
|
} {
|
|
t.Run(tt.name+"_"+tt.want, func(t *testing.T) {
|
|
if got := interfaceClass(tt.name, tt.virtual); got != tt.want {
|
|
t.Errorf("interfaceClass(%q, %v) = %q, want %q", tt.name, tt.virtual, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_noiseDelta(t *testing.T) {
|
|
en0 := deltaTestInterface{name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1500, ips: []string{"192.0.2.10/24"}}
|
|
en5 := deltaTestInterface{name: "en5", up: true, mac: "aa:bb:cc:dd:ee:05", mtu: 1500, ips: []string{"192.0.2.50/24"}}
|
|
awdl0 := deltaTestInterface{name: "awdl0", up: true, mac: "aa:bb:cc:dd:ee:02", mtu: 1484, ips: []string{"fe80::1/64"}}
|
|
awdl0NewMAC := deltaTestInterface{name: "awdl0", up: true, mac: "aa:bb:cc:dd:ee:03", mtu: 1484, ips: []string{"fe80::1/64"}}
|
|
en0Extra := deltaTestInterface{
|
|
name: "en0", up: true, mac: "aa:bb:cc:dd:ee:01", mtu: 1500,
|
|
ips: []string{"192.0.2.10/24", "192.0.2.11/24"},
|
|
}
|
|
|
|
docker0 := deltaTestInterface{name: "docker0", up: true, mtu: 1500}
|
|
docker0NewMTU := deltaTestInterface{name: "docker0", up: true, mtu: 1400}
|
|
|
|
for _, tt := range []struct {
|
|
name string
|
|
routeOld string
|
|
routeNew string
|
|
before []deltaTestInterface
|
|
after []deltaTestInterface
|
|
virtual []string
|
|
timeJumped bool
|
|
beforeV4, beforeV6 bool
|
|
afterV4, afterV6 bool
|
|
source4 string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "airdrop_only",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0NewMAC},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "hardware_and_airdrop",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0Extra, awdl0NewMAC},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "no_change",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "default_route_moved",
|
|
routeOld: "en0", routeNew: "en5",
|
|
before: []deltaTestInterface{en0, en5, awdl0},
|
|
after: []deltaTestInterface{en0, en5, awdl0NewMAC},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "virtual_only",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, docker0},
|
|
after: []deltaTestInterface{en0, docker0NewMTU},
|
|
virtual: []string{"docker0"},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "a_time_jump_is_never_noise",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0NewMAC},
|
|
timeJumped: true,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "the_default_route_runs_over_a_virtual_interface",
|
|
routeOld: "docker0", routeNew: "docker0",
|
|
before: []deltaTestInterface{en0, docker0},
|
|
after: []deltaTestInterface{en0, docker0NewMTU},
|
|
virtual: []string{"docker0"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "a_virtual_interface_holds_a_routable_address",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, {name: "docker0", up: true, mtu: 1500, ips: []string{"172.17.0.1/16"}}},
|
|
after: []deltaTestInterface{en0, {name: "docker0", up: true, mtu: 1400, ips: []string{"172.17.0.1/16"}}},
|
|
virtual: []string{"docker0"},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "an_airdrop_interface_holds_the_resolver_source",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, {name: "awdl0", up: true, mtu: 1484, ips: []string{"169.254.10.5/16"}}},
|
|
after: []deltaTestInterface{en0, {name: "awdl0", up: true, mtu: 1400, ips: []string{"169.254.10.5/16"}}},
|
|
source4: "169.254.10.5",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "have_v4_flips",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0NewMAC},
|
|
beforeV4: true,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "have_v6_flips",
|
|
routeOld: "en0", routeNew: "en0",
|
|
before: []deltaTestInterface{en0, awdl0},
|
|
after: []deltaTestInterface{en0, awdl0NewMAC},
|
|
afterV6: true,
|
|
want: false,
|
|
},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
stubSnapshotVirtualSet(t, tt.virtual...)
|
|
stubResolverSource(t, tt.source4)
|
|
before := deltaTestState(t, tt.routeOld, tt.before...)
|
|
after := deltaTestState(t, tt.routeNew, tt.after...)
|
|
before.HaveV4, before.HaveV6 = tt.beforeV4, tt.beforeV6
|
|
after.HaveV4, after.HaveV6 = tt.afterV4, tt.afterV6
|
|
changes := diffNetworkDelta(before, after)
|
|
if got := noiseDelta(before, after, tt.timeJumped, changes); got != tt.want {
|
|
t.Errorf("noiseDelta = %v, want %v, changes %+v", got, tt.want, changes)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// stubResolverSource sets the address that the resolver sends its queries
|
|
// from. The tests own that global, so each case starts from a known source.
|
|
func stubResolverSource(t *testing.T, source4 string) {
|
|
t.Helper()
|
|
original4, original6 := ctrld.GetDefaultLocalIPv4(), ctrld.GetDefaultLocalIPv6()
|
|
t.Cleanup(func() {
|
|
ctrld.SetDefaultLocalIPv4(context.Background(), original4)
|
|
ctrld.SetDefaultLocalIPv6(context.Background(), original6)
|
|
})
|
|
ctrld.SetDefaultLocalIPv4(context.Background(), net.ParseIP(source4))
|
|
ctrld.SetDefaultLocalIPv6(context.Background(), nil)
|
|
}
|