package networkconfig import ( "net/netip" "reflect" "testing" ) func testConfig() Config { return Config{Interfaces: []Interface{ {Name: "Interface-1", Device: "eth0", Role: RoleInternal, Address: "192.168.0.223/24", Weight: 1}, {Name: "Interface-2", Device: "eth1", Role: RoleExternal, Address: "192.168.78.55/24", Gateway: "192.168.78.1", Weight: 1}, }} } func TestNormalizeAndAutomaticTrafficClassification(t *testing.T) { config, err := Normalize(testConfig()) if err != nil { t.Fatal(err) } if config.RoutingMode != RoutingModeActive || config.RollbackTimeoutSecond != 120 { t.Fatalf("normalized defaults = %+v", config) } internal, err := RouteFor(config, netip.MustParseAddr("192.168.0.11")) if err != nil || !reflect.DeepEqual(internal, []string{"eth0"}) { t.Fatalf("syslog route = %v, %v", internal, err) } external, err := RouteFor(config, netip.MustParseAddr("203.248.240.140")) if err != nil || !reflect.DeepEqual(external, []string{"eth1"}) { t.Fatalf("NTP route = %v, %v", external, err) } } func TestNormalizeKeepsFixedLogicalInterfaceMapping(t *testing.T) { config := testConfig() config.Interfaces[0].Device = Interface2Device config.Interfaces[1].Device = Interface1Device if _, err := Normalize(config); err == nil { t.Fatal("Normalize accepted reversed Interface-1/Interface-2 device mapping") } } func TestTwoExternalInterfacesUseBothDefaultNextHops(t *testing.T) { config := testConfig() config.Interfaces[0].Role = RoleExternal config.Interfaces[0].Gateway = "192.168.0.1" route, err := RouteFor(config, netip.MustParseAddr("8.8.8.8")) if err != nil || !reflect.DeepEqual(route, []string{"eth0", "eth1"}) { t.Fatalf("active/active route = %v, %v", route, err) } } func TestTwoInternalInterfacesHaveNoDefaultRoute(t *testing.T) { config := testConfig() config.Interfaces[1].Role = RoleInternal config.Interfaces[1].Gateway = "" route, err := RouteFor(config, netip.MustParseAddr("8.8.8.8")) if err != nil || len(route) != 0 { t.Fatalf("internal-only route = %v, %v", route, err) } } func TestNormalizeRejectsAmbiguousAndUnsafeInputs(t *testing.T) { tests := []struct { name string mutate func(*Config) }{ {"overlapping subnets", func(c *Config) { c.Interfaces[1].Address = "192.168.0.55/24"; c.Interfaces[1].Gateway = "192.168.0.1" }}, {"external without gateway", func(c *Config) { c.Interfaces[1].Gateway = "" }}, {"gateway outside subnet", func(c *Config) { c.Interfaces[1].Gateway = "192.168.79.1" }}, {"unknown device", func(c *Config) { c.Interfaces[1].Device = "eth2" }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { config := testConfig() test.mutate(&config) if _, err := Normalize(config); err == nil { t.Fatal("Normalize accepted invalid configuration") } }) } } func TestNormalizeSupportsHostDHCPDNSIPv6AndOOB(t *testing.T) { config := testConfig() config.HostName = "Console-Gateway" config.OperatingMode = OperatingModeOOB config.OOBPrimaryInterface = "Interface-1" config.OOBFallbackInterface = "Interface-2" config.OOBHealthCheckTarget = "9.9.9.9" config.OOBHealthCheckSecond = 15 config.OOBFailureThreshold = 4 config.Interfaces[0].DNSServers = []string{"192.168.0.1", "8.8.8.8"} config.Interfaces[0].IPv6Mode = IPv6ModeStatic config.Interfaces[0].IPv6Address = "2001:db8::223/64" config.Interfaces[0].IPv6Gateway = "2001:db8::1" config.Interfaces[0].IPv6DNSServers = []string{"2001:4860:4860::8888"} config.Interfaces[0].Gateway = "192.168.0.1" normalized, err := Normalize(config) if err != nil { t.Fatal(err) } if normalized.HostName != "console-gateway" || normalized.OperatingMode != OperatingModeOOB || normalized.RoutingMode != OperatingModeOOB { t.Fatalf("normalized host/mode = %+v", normalized) } if normalized.OOBPrimaryInterface != "Interface-1" || normalized.OOBFallbackInterface != "Interface-2" { t.Fatalf("normalized OOB interfaces = %+v", normalized) } if normalized.Interfaces[0].IPv6Address != "2001:db8::223/64" || len(normalized.Interfaces[0].IPv6DNSServers) != 1 { t.Fatalf("normalized IPv6 interface = %+v", normalized.Interfaces[0]) } } func TestNormalizeRejectsOOBWithoutTwoStaticGatewayPaths(t *testing.T) { tests := []struct { name string mutate func(*Config) }{ {"primary dhcp", func(c *Config) { c.Interfaces[0].AddressMode = AddressModeDHCP c.Interfaces[0].Address = "" c.Interfaces[0].Gateway = "" }}, {"fallback dhcp", func(c *Config) { c.Interfaces[1].AddressMode = AddressModeDHCP c.Interfaces[1].Address = "" c.Interfaces[1].Gateway = "" }}, {"primary gateway missing", func(c *Config) { c.Interfaces[0].Gateway = "" }}, {"fallback gateway missing", func(c *Config) { c.Interfaces[1].Gateway = "" }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { config := testConfig() config.OperatingMode = OperatingModeOOB config.Interfaces[0].Gateway = "192.168.0.1" test.mutate(&config) if _, err := Normalize(config); err == nil { t.Fatal("Normalize accepted OOB without two usable static gateway paths") } }) } } func TestNormalizeRejectsInvalidHostDNSAndIPv6(t *testing.T) { for _, test := range []struct { name string mutate func(*Config) }{ {"host", func(c *Config) { c.HostName = "invalid host" }}, {"ipv4 dns family", func(c *Config) { c.Interfaces[0].DNSServers = []string{"2001:db8::1"} }}, {"ipv6 dns family", func(c *Config) { c.Interfaces[0].IPv6DNSServers = []string{"8.8.8.8"} }}, {"static ipv6 missing address", func(c *Config) { c.Interfaces[0].IPv6Mode = IPv6ModeStatic }}, } { t.Run(test.name, func(t *testing.T) { config := testConfig() test.mutate(&config) if _, err := Normalize(config); err == nil { t.Fatal("Normalize accepted invalid extended network configuration") } }) } } func TestNormalizeRejectsOOBWithAggregateMode(t *testing.T) { config := testConfig() config.OperatingMode = OperatingModeOOB config.Advanced.Mode = AdvancedModeBonding config.Interfaces[0].Gateway = "192.168.0.1" if _, err := Normalize(config); err == nil { t.Fatal("Normalize accepted OOB together with bonding") } } func TestRouteForUsesAggregateDevice(t *testing.T) { config := testConfig() config.Advanced = AdvancedConfig{Mode: AdvancedModeBridge} devices, err := RouteFor(config, netip.MustParseAddr("192.168.0.10")) if err != nil || !reflect.DeepEqual(devices, []string{BridgeDevice}) { t.Fatalf("bridge route = %v, %v", devices, err) } }