package matrix import ( "fmt" "net/netip" "strings" "testing" "housecarl/internal/api" "housecarl/internal/model" ) func state() *model.State { st := model.NewState() st.Zones = []*model.Zone{ {ID: 1, Name: "guest", Interfaces: []string{"eth2"}}, {ID: 2, Name: "lan", Interfaces: []string{"eth1"}}, {ID: 3, Name: "wan", Interfaces: []string{"wan"}, External: true}, } st.Sets = []*model.AddressSet{{ID: 4, Name: "admins", Entries: []string{"192.168.1.0/28"}}} pos := 0.0 rule := func(r model.Rule) { pos++ r.ID, r.Position = int64(10+pos), pos st.Rules = append(st.Rules, &r) } rule(model.Rule{Chain: "input", Match: model.Match{Sources: []string{"admins"}, Services: []string{"ssh"}}, Action: "accept", Comment: "SSH"}) rule(model.Rule{Chain: "input", Match: model.Match{InZones: []string{"lan", "guest"}, Services: []string{"dns"}}, Action: "accept"}) rule(model.Rule{Chain: "forward", Match: model.Match{InZones: []string{"lan"}, OutZones: []string{"wan"}}, Action: "accept", Comment: "LAN out"}) rule(model.Rule{Chain: "forward", Match: model.Match{InZones: []string{"guest"}, OutZones: []string{"wan"}, Services: []string{"http", "https"}}, Action: "accept"}) rule(model.Rule{Chain: "forward", Match: model.Match{InZones: []string{"wan"}}, Action: "drop", Comment: "nothing in"}) rule(model.Rule{Chain: "forward", Match: model.Match{InZones: []string{"lan"}}, CtState: []string{"established"}, Action: "drop"}) // not for new rule(model.Rule{Chain: "output", Match: model.Match{OutZones: []string{"wan"}}, Action: "drop", Disabled: true}) st.Forwards = []*model.PortForward{{ID: 30, Name: "web", Protocols: []string{"tcp"}, Ports: "443", To: "192.168.1.10"}} st.Settings.Builtins.MSSClamp = true return st } func find(m api.Matrix, from, to string) api.MatrixCell { for _, c := range m.Cells { if c.From == from && c.To == to { return c } } return api.MatrixCell{} } func TestMatrix(t *testing.T) { m := Build(state()) if strings.Join(m.Zones, ",") != "guest,lan,wan" || len(m.Cells) != 3*3+3+3 { t.Fatalf("zones %v, %d cells", m.Zones, len(m.Cells)) } for _, c := range []struct { from, to, verdict, decided string exceptions int }{ {"lan", "wan", "allowed", "rule forward #13 (LAN out)", 0}, {"guest", "wan", "blocked", "forward policy", 1}, // only web {"wan", "lan", "blocked", "rule forward #15 (nothing in)", 2}, // antispoof, port forwards come first {"lan", "guest", "blocked", "forward policy", 0}, // the established-only rule is no exception {"lan", Firewall, "blocked", "input policy", 4}, // icmpv6, ping, ssh from admins, dns {"wan", Firewall, "blocked", "input policy", 4}, // antispoof, icmpv6, dhcpv6, ssh from admins {Firewall, "wan", "allowed", "output policy", 0}, // the drop is disabled } { got := find(m, c.from, c.to) if got.Verdict != c.verdict || got.DecidedBy != c.decided || len(got.Exceptions) != c.exceptions { t.Errorf("%s → %s: %s by %s, %d exceptions %+v; want %s by %s, %d", c.from, c.to, got.Verdict, got.DecidedBy, len(got.Exceptions), got.Exceptions, c.verdict, c.decided, c.exceptions) } } g := find(m, "guest", "wan").Exceptions[0] if g.Verdict != "allowed" || g.What != "http, https" || g.RuleID != 14 { t.Errorf("the guest exception: %+v", g) } if e := find(m, "wan", "lan").Exceptions; e[0].Label != "built-in: antispoof" || e[1].Label != "port forwards" { t.Errorf("wan → lan exceptions: %+v", e) } if _, ok := Cell(state(), Options{}, "lan", "nowhere"); ok { t.Error("an unknown zone") } } // Rules whose addresses all live behind another zone are left out of a // pair; routed (unknown) networks, and mixed lists, stay. func TestConnected(t *testing.T) { st := state() st.Sets = append(st.Sets, &model.AddressSet{ID: 5, Name: "guest_net", Entries: []string{"192.168.2.0/24"}}, &model.AddressSet{ID: 6, Name: "mixed", Entries: []string{"192.168.2.0/24", "10.20.30.0/24"}}) st.Rules = append(st.Rules, &model.Rule{ID: 40, Chain: "forward", Position: 10, Match: model.Match{Sources: []string{"guest_net"}}, Action: "drop", Comment: "guests"}, &model.Rule{ID: 41, Chain: "forward", Position: 11, Match: model.Match{Destinations: []string{"192.168.2.5"}}, Action: "accept", Comment: "to a guest"}, &model.Rule{ID: 42, Chain: "forward", Position: 12, Match: model.Match{Sources: []string{"mixed"}}, Action: "drop", Comment: "mixed"}) o := Options{Connected: map[netip.Prefix]string{netip.MustParsePrefix("192.168.1.0/24"): "lan", netip.MustParsePrefix("192.168.2.0/24"): "guest"}} ids := func(c api.MatrixCell) []int64 { var out []int64 for _, e := range c.Exceptions { out = append(out, e.RuleID) } return out } lanWan, _ := Cell(st, o, "lan", "wan") if fmt.Sprint(ids(lanWan)) != "[]" { t.Errorf("lan → wan: %v", ids(lanWan)) // decided by "LAN out" before the others anyway } lanGuest, _ := Cell(st, o, "lan", "guest") if fmt.Sprint(ids(lanGuest)) != "[41 42]" { t.Errorf("lan → guest: %v", ids(lanGuest)) // not "guests": their addresses are guest's } guestLan, _ := Cell(st, o, "guest", "lan") if fmt.Sprint(ids(guestLan)) != "[40 42]" { t.Errorf("guest → lan: %v", ids(guestLan)) // not "to a guest": it is in guest, not lan } }