package validate import ( "net/netip" "strings" "testing" "steward/internal/model" ) func pool(s string) model.Pool { p, err := model.ParsePool(s) if err != nil { panic(err) } return p } func good() *model.State { return &model.State{ Settings: model.DefaultSettings(), Subnets: []*model.Subnet{ {ID: 1, Name: "office", Interface: "eth0", Prefix: netip.MustParsePrefix("10.0.10.0/24"), Pools: []model.Pool{pool("10.0.10.100-10.0.10.199")}, Options: []model.OptionValue{{Code: 3, Value: "10.0.10.1"}}}, {ID: 2, Name: "iot", Interface: "eth0.20", Prefix: netip.MustParsePrefix("10.0.20.0/24")}, }, Reservations: []*model.Reservation{ {ID: 3, MAC: "aa:bb:cc:dd:ee:01", IP: netip.MustParseAddr("10.0.10.20"), Hostname: "nas"}, }, } } func codes(ps []Problem) string { var out []string for _, p := range ps { out = append(out, p.Severity+":"+p.Code) } return strings.Join(out, " ") } func TestGoodConfig(t *testing.T) { st := good() st.Subnets[1].Pools = []model.Pool{pool("10.0.20.100-10.0.20.200")} if ps := Check(st, nil); len(ps) != 0 { t.Errorf("problems: %v", ps) } } func TestProblems(t *testing.T) { cases := []struct { name string edit func(st *model.State) want string }{ {"host bits", func(st *model.State) { st.Subnets[0].Prefix = netip.MustParsePrefix("10.0.10.5/24") }, "error:subnet_prefix"}, {"too small", func(st *model.State) { st.Subnets[0].Prefix = netip.MustParsePrefix("10.0.10.0/31") }, "error:subnet_size"}, {"ipv6", func(st *model.State) { st.Subnets[0].Prefix = netip.MustParsePrefix("2001:db8::/64") }, "error:subnet_prefix"}, {"overlap", func(st *model.State) { st.Subnets[1].Prefix = netip.MustParsePrefix("10.0.0.0/16") }, "error:subnet_overlap"}, {"name taken", func(st *model.State) { st.Subnets[1].Name = "Office" }, "error:subnet_name_taken"}, {"no interface", func(st *model.State) { st.Subnets[0].Interface = "" }, "error:subnet_interface_missing"}, {"bad interface", func(st *model.State) { st.Subnets[0].Interface = "eth 0" }, "error:subnet_interface_name"}, {"no pools", func(st *model.State) {}, "warning:subnet_no_pools"}, {"pool outside", func(st *model.State) { st.Subnets[0].Pools = []model.Pool{pool("10.0.11.1-10.0.11.9")} }, "error:pool_outside_subnet"}, {"pool network", func(st *model.State) { st.Subnets[0].Pools = []model.Pool{pool("10.0.10.0/24")} }, "error:pool_network_address"}, {"pool overlap", func(st *model.State) { st.Subnets[0].Pools = append(st.Subnets[0].Pools, pool("10.0.10.150-10.0.10.250")) }, "error:pool_overlap"}, {"lease time", func(st *model.State) { st.Subnets[0].LeaseTime = 10 }, "error:lease_time_range"}, {"reserved option", func(st *model.State) { st.Settings.Options = []model.OptionValue{{Code: 51, Value: "1"}} }, "error:option_reserved"}, {"bad option", func(st *model.State) { st.Subnets[0].Options = []model.OptionValue{{Code: 6, Value: "dns.example"}} }, "error:option_invalid"}, {"duplicate option", func(st *model.State) { st.Subnets[0].Options = append(st.Subnets[0].Options, model.OptionValue{Code: 3, Value: "10.0.10.2"}) }, "error:option_duplicate"}, {"router outside", func(st *model.State) { st.Subnets[0].Options = []model.OptionValue{{Code: 3, Value: "10.0.11.1"}} }, "warning:router_outside_subnet"}, {"router in pool", func(st *model.State) { st.Subnets[0].Options = []model.OptionValue{{Code: 3, Value: "10.0.10.150"}} }, "warning:router_in_pool"}, {"no client", func(st *model.State) { st.Reservations[0].MAC = "" }, "error:reservation_no_client"}, {"no subnet", func(st *model.State) { st.Reservations[0].IP = netip.MustParseAddr("192.168.1.5") }, "error:reservation_no_subnet"}, {"broadcast", func(st *model.State) { st.Reservations[0].IP = netip.MustParseAddr("10.0.10.255") }, "error:reservation_network_address"}, {"ip taken", func(st *model.State) { st.Reservations = append(st.Reservations, &model.Reservation{ID: 9, MAC: "aa:bb:cc:dd:ee:02", IP: netip.MustParseAddr("10.0.10.20")}) }, "error:reservation_ip_taken"}, {"client taken", func(st *model.State) { st.Reservations = append(st.Reservations, &model.Reservation{ID: 9, MAC: "aa:bb:cc:dd:ee:01", IP: netip.MustParseAddr("10.0.10.21")}) }, "error:reservation_client_taken"}, {"hostname", func(st *model.State) { st.Reservations[0].Hostname = "my_nas" }, "error:reservation_hostname"}, } for _, c := range cases { st := good() st.Subnets[1].Pools = []model.Pool{pool("10.0.20.100-10.0.20.200")} if c.name == "no pools" { st.Subnets[1].Pools = nil } c.edit(st) got := codes(Check(st, nil)) if !strings.Contains(" "+got+" ", " "+c.want+" ") { t.Errorf("%s: got %q, want %s", c.name, got, c.want) } } } func TestDisabledReservationsDontConflict(t *testing.T) { st := good() st.Subnets[1].Pools = []model.Pool{pool("10.0.20.100-10.0.20.200")} st.Reservations[0].Disabled = true // The same client and address again, enabled: fine while the first is // disabled. st.Reservations = append(st.Reservations, &model.Reservation{ID: 9, MAC: "aa:bb:cc:dd:ee:01", IP: netip.MustParseAddr("10.0.10.20")}) if ps := Check(st, nil); len(ps) != 0 { t.Errorf("problems: %v", ps) } st.Reservations[0].Disabled = false if got := codes(Check(st, nil)); !strings.Contains(got, "reservation_ip_taken") { t.Errorf("enabling it again: %q", got) } } func TestSameMACInTwoSubnets(t *testing.T) { st := good() st.Subnets[1].Pools = []model.Pool{pool("10.0.20.100-10.0.20.200")} st.Reservations = append(st.Reservations, &model.Reservation{ID: 9, MAC: "aa:bb:cc:dd:ee:01", IP: netip.MustParseAddr("10.0.20.20")}) if ps := Check(st, nil); len(ps) != 0 { t.Errorf("a laptop on two VLANs: %v", ps) } } func TestHostInterfaces(t *testing.T) { st := good() st.Subnets[1].Pools = []model.Pool{pool("10.0.20.100-10.0.20.200")} st.Subnets[0].Pools = []model.Pool{pool("10.0.10.1-10.0.10.199")} st.Subnets[0].Options = nil env := &Env{Interfaces: map[string][]netip.Prefix{ "eth0": {netip.MustParsePrefix("10.0.10.2/24")}, "wg0": {netip.MustParsePrefix("10.9.0.1/24")}, }} got := codes(Check(st, env)) for _, want := range []string{"warning:pool_includes_server", "warning:interface_unknown"} { if !strings.Contains(got, want) { t.Errorf("got %q, want %s", got, want) } } env.Interfaces["eth0.20"] = []netip.Prefix{netip.MustParsePrefix("192.168.5.1/24")} if got := codes(Check(st, env)); !strings.Contains(got, "warning:interface_no_address") { t.Errorf("got %q, want interface_no_address", got) } }