go-network-configurator/networkConfigurator_test.go
2026-07-01 15:17:11 -05:00

349 lines
11 KiB
Go

package netconfig
import (
"context"
"errors"
"net"
"testing"
)
func mustCIDR(t *testing.T, s string) *net.IPNet {
t.Helper()
_, n, err := net.ParseCIDR(s)
if err != nil {
t.Fatalf("ParseCIDR(%q): %v", s, err)
}
return n
}
func TestRouteString(t *testing.T) {
r := &Route{
Destination: mustCIDR(t, "10.0.0.0/24"),
Gateway: net.ParseIP("10.0.0.1"),
Metric: 100,
}
if got, want := r.String(), "10.0.0.0/24 via 10.0.0.1 metric 100"; got != want {
t.Errorf("Route.String() = %q, want %q", got, want)
}
}
func TestInterfaceString(t *testing.T) {
mac, _ := net.ParseMAC("00:11:22:33:44:55")
iface := &Interface{
Name: "eth0",
MAC: mac,
Addresses: []*net.IPNet{mustCIDR(t, "192.168.1.10/24")},
Gateway4: net.ParseIP("192.168.1.1"),
Routes: []*Route{
{Destination: mustCIDR(t, "10.0.0.0/24"), Gateway: net.ParseIP("10.0.0.1"), Metric: 5},
},
}
got := iface.String()
want := "Name: eth0 MAC: 00:11:22:33:44:55 Addresses: [192.168.1.0/24] Gateway4: 192.168.1.1 Gateway6: <nil> Routes: [10.0.0.0/24 via 10.0.0.1 metric 5]"
if got != want {
t.Errorf("Interface.String()\n got %q\nwant %q", got, want)
}
}
func TestFindInterfaceByName(t *testing.T) {
eth0 := &Interface{Name: "eth0", Gateway4: net.ParseIP("203.0.113.1")}
eth1 := &Interface{Name: "eth1"}
pub6 := &Interface{Name: "eth2", Gateway6: net.ParseIP("2001:db8::1")}
ifaces := []*Interface{eth1, eth0, pub6}
t.Run("by name", func(t *testing.T) {
if got := FindInterfaceByName("eth1", ifaces); got != eth1 {
t.Errorf("got %v, want eth1", got)
}
})
t.Run("public picks gateway4 interface", func(t *testing.T) {
if got := FindInterfaceByName(Public, ifaces); got != eth0 {
t.Errorf("got %v, want eth0", got)
}
})
t.Run("public6 picks gateway6 interface", func(t *testing.T) {
if got := FindInterfaceByName(Public6, ifaces); got != pub6 {
t.Errorf("got %v, want pub6", got)
}
})
t.Run("not found", func(t *testing.T) {
if got := FindInterfaceByName("missing", ifaces); got != nil {
t.Errorf("got %v, want nil", got)
}
})
t.Run("public with no gateway returns nil", func(t *testing.T) {
if got := FindInterfaceByName(Public, []*Interface{eth1}); got != nil {
t.Errorf("got %v, want nil", got)
}
})
t.Run("public6 with no gateway returns nil", func(t *testing.T) {
if got := FindInterfaceByName(Public6, []*Interface{eth1}); got != nil {
t.Errorf("got %v, want nil", got)
}
})
}
func TestReorderPrimaryAddress(t *testing.T) {
// addrStrings renders a list of addresses for stable comparison.
addrStrings := func(addrs []*net.IPNet) []string {
out := make([]string, len(addrs))
for i, a := range addrs {
out[i] = a.IP.String()
}
return out
}
equal := func(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// hostIP keeps the host bits so IP.Equal matching is exercised, unlike the
// network address returned by mustCIDR.
hostIP := func(ip, cidr string) *net.IPNet {
n := mustCIDR(t, cidr)
return &net.IPNet{IP: net.ParseIP(ip), Mask: n.Mask}
}
a := hostIP("192.168.1.10", "192.168.1.10/24")
b := hostIP("192.168.1.11", "192.168.1.11/24")
c := hostIP("192.168.1.12", "192.168.1.12/24")
v6 := hostIP("2001:db8::5", "2001:db8::5/64")
t.Run("middle moves to front", func(t *testing.T) {
got, ok := reorderPrimaryAddress([]*net.IPNet{a, b, c}, b)
if !ok {
t.Fatal("ok = false, want true")
}
if want := []string{"192.168.1.11", "192.168.1.10", "192.168.1.12"}; !equal(addrStrings(got), want) {
t.Errorf("got %v, want %v", addrStrings(got), want)
}
})
t.Run("already first is unchanged", func(t *testing.T) {
got, ok := reorderPrimaryAddress([]*net.IPNet{a, b, c}, a)
if !ok {
t.Fatal("ok = false, want true")
}
if want := []string{"192.168.1.10", "192.168.1.11", "192.168.1.12"}; !equal(addrStrings(got), want) {
t.Errorf("got %v, want %v", addrStrings(got), want)
}
})
t.Run("v6 target preserves v4 relative order", func(t *testing.T) {
got, ok := reorderPrimaryAddress([]*net.IPNet{a, b, v6}, v6)
if !ok {
t.Fatal("ok = false, want true")
}
if want := []string{"2001:db8::5", "192.168.1.10", "192.168.1.11"}; !equal(addrStrings(got), want) {
t.Errorf("got %v, want %v", addrStrings(got), want)
}
})
t.Run("absent target returns false", func(t *testing.T) {
got, ok := reorderPrimaryAddress([]*net.IPNet{a, b}, c)
if ok {
t.Error("ok = true, want false")
}
// The original slice is returned unchanged.
if want := []string{"192.168.1.10", "192.168.1.11"}; !equal(addrStrings(got), want) {
t.Errorf("got %v, want %v", addrStrings(got), want)
}
})
}
// fakeIfaceBackend records the calls made to it and optionally returns an error.
type fakeIfaceBackend struct {
addrCalls int
routeCalls int
dnsCalls int
lastAddrs []*net.IPNet
err error
}
func (f *fakeIfaceBackend) SetIfaceAddresses(_ context.Context, iface string, addrs []*net.IPNet, gateway4, gateway6 net.IP) error {
f.addrCalls++
f.lastAddrs = addrs
return f.err
}
func (f *fakeIfaceBackend) SetIfaceRoutes(_ context.Context, iface string, routes []*Route) error {
f.routeCalls++
return f.err
}
func (f *fakeIfaceBackend) SetIfaceDNS(_ context.Context, iface string, servers []net.IP, searchDomains []string) error {
f.dnsCalls++
return f.err
}
// fakePanelBackend records the calls made to it and optionally returns an error.
type fakePanelBackend struct {
reloadCalls int
setMainIPCalls int
removeIPCalls int
err error
}
func (f *fakePanelBackend) reload(_ context.Context) error {
f.reloadCalls++
return f.err
}
func (f *fakePanelBackend) setMainIP(_ context.Context, addr net.IP) error {
f.setMainIPCalls++
return f.err
}
func (f *fakePanelBackend) removeIP(_ context.Context, addr net.IP) error {
f.removeIPCalls++
return f.err
}
// Every registered backend must be invoked even when an earlier backend
// returns an error, since each writes an independent configuration.
func TestApplyIfaceAddresses(t *testing.T) {
failing := &fakeIfaceBackend{err: errors.New("boom")}
ok := &fakeIfaceBackend{}
backends := []namedIfaceBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
applyIfaceAddresses(context.Background(), backends, "eth0", nil, nil, nil)
if failing.addrCalls != 1 || ok.addrCalls != 1 {
t.Errorf("addr calls: failing=%d ok=%d, want 1 each", failing.addrCalls, ok.addrCalls)
}
}
func TestApplyIfaceRoutes(t *testing.T) {
failing := &fakeIfaceBackend{err: errors.New("boom")}
ok := &fakeIfaceBackend{}
backends := []namedIfaceBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
applyIfaceRoutes(context.Background(), backends, "eth0", nil)
if failing.routeCalls != 1 || ok.routeCalls != 1 {
t.Errorf("route calls: failing=%d ok=%d, want 1 each", failing.routeCalls, ok.routeCalls)
}
}
func TestReloadPanels(t *testing.T) {
failing := &fakePanelBackend{err: errors.New("boom")}
ok := &fakePanelBackend{}
backends := []namedPanelBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
reloadPanels(context.Background(), backends)
if failing.reloadCalls != 1 || ok.reloadCalls != 1 {
t.Errorf("reload calls: failing=%d ok=%d, want 1 each", failing.reloadCalls, ok.reloadCalls)
}
}
func TestSetMainIPOnPanels(t *testing.T) {
failing := &fakePanelBackend{err: errors.New("boom")}
ok := &fakePanelBackend{}
backends := []namedPanelBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
setMainIPOnPanels(context.Background(), backends, net.ParseIP("192.0.2.5"))
if failing.setMainIPCalls != 1 || ok.setMainIPCalls != 1 {
t.Errorf("setMainIP calls: failing=%d ok=%d, want 1 each", failing.setMainIPCalls, ok.setMainIPCalls)
}
}
func TestRemoveIPFromPanels(t *testing.T) {
failing := &fakePanelBackend{err: errors.New("boom")}
ok := &fakePanelBackend{}
backends := []namedPanelBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
removeIPFromPanels(context.Background(), backends, net.ParseIP("192.0.2.5"))
if failing.removeIPCalls != 1 || ok.removeIPCalls != 1 {
t.Errorf("removeIP calls: failing=%d ok=%d, want 1 each", failing.removeIPCalls, ok.removeIPCalls)
}
}
// fakeDNSReaderBackend is a file backend that also reports back persisted
// interfaces (including DNS), satisfying both ifaceBackend and ifaceDNSReader.
type fakeDNSReaderBackend struct {
ifaces []*Interface
err error
}
func (f *fakeDNSReaderBackend) SetIfaceAddresses(context.Context, string, []*net.IPNet, net.IP, net.IP) error {
return nil
}
func (f *fakeDNSReaderBackend) SetIfaceRoutes(context.Context, string, []*Route) error { return nil }
func (f *fakeDNSReaderBackend) SetIfaceDNS(context.Context, string, []net.IP, []string) error {
return nil
}
func (f *fakeDNSReaderBackend) GetInterfaces() ([]*Interface, error) { return f.ifaces, f.err }
// mergeDNSFromBackends must union DNS from every backend into the matching
// runtime interface (by name), de-duplicate entries so multi-backend hosts do
// not list a resolver twice, skip backends with no matching interface, and keep
// going when one backend errors.
func TestMergeDNSFromBackends(t *testing.T) {
runtime := []*Interface{{Name: "eth0"}, {Name: "eth1"}}
backends := []namedIfaceBackend{
{name: "broken", backend: &fakeDNSReaderBackend{err: errors.New("boom")}},
{name: "netplan", backend: &fakeDNSReaderBackend{ifaces: []*Interface{
{Name: "eth0", DNS: []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("2001:4860:4860::8888")}, SearchDomains: []string{"example.com"}},
}}},
{name: "cloud-init", backend: &fakeDNSReaderBackend{ifaces: []*Interface{
// Overlaps netplan (must dedupe) and adds one new entry.
{Name: "eth0", DNS: []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("1.1.1.1")}, SearchDomains: []string{"example.com", "corp.example"}},
}}},
{name: "ifupdown", backend: &fakeDNSReaderBackend{ifaces: []*Interface{
{Name: "eth1", DNS: []net.IP{net.ParseIP("9.9.9.9")}, SearchDomains: []string{"a.test"}},
// No runtime match; must be ignored.
{Name: "eth9", DNS: []net.IP{net.ParseIP("10.0.0.1")}},
}}},
}
mergeDNSFromBackends(backends, runtime)
eth0 := runtime[0]
wantDNS := []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("2001:4860:4860::8888"), net.ParseIP("1.1.1.1")}
if !equalIPs(eth0.DNS, wantDNS) {
t.Errorf("eth0 DNS = %v, want %v", eth0.DNS, wantDNS)
}
wantSearch := []string{"example.com", "corp.example"}
if len(eth0.SearchDomains) != len(wantSearch) || eth0.SearchDomains[0] != wantSearch[0] || eth0.SearchDomains[1] != wantSearch[1] {
t.Errorf("eth0 SearchDomains = %v, want %v", eth0.SearchDomains, wantSearch)
}
eth1 := runtime[1]
if !equalIPs(eth1.DNS, []net.IP{net.ParseIP("9.9.9.9")}) {
t.Errorf("eth1 DNS = %v, want [9.9.9.9]", eth1.DNS)
}
if len(eth1.SearchDomains) != 1 || eth1.SearchDomains[0] != "a.test" {
t.Errorf("eth1 SearchDomains = %v, want [a.test]", eth1.SearchDomains)
}
}
func equalIPs(a, b []net.IP) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !a[i].Equal(b[i]) {
return false
}
}
return true
}