Zone matrix: what the rules do between zones

internal/matrix walks each pair's rules in order, as nftables would, for
new connections: zone → zone (forward), zone → the firewall (input) and
back (output), with the built-ins that act on new connections. A rule
for the whole pair decides it; one for part of its traffic (addresses,
services, times, rates, a quota) is an exception; the rest falls to the
policy. Rules whose addresses all lie in networks connected to other
zones are left out of a pair. GET /matrix (with ?changeset, pending
changes), hc matrix [FROM TO], and a Matrix page whose cells open their
rules.

e2e: 166 checks, among them the matrix of the maintainer's imported
ruleset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-19 20:35:26 +01:00
1 parent e9cbb3e4e9
commit b6f31c86a6
17 files changed
+725 -2

No files matched your search

+37
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"net/netip"
"sort"
"strings"
@@ -12,6 +13,7 @@ import (
"housecarl/internal/api"
"housecarl/internal/auth"
"housecarl/internal/engine"
"housecarl/internal/matrix"
"housecarl/internal/model"
"housecarl/internal/store"
)
@@ -416,6 +418,41 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request, p *auth.
return s.change(w, r, p, model.Op{Op: model.OpSettings, Data: data})
}
// ---- the zone matrix
// zoneMatrix answers what the rules do to new connections between zones
// (internal/matrix); with a changeset named, as it will make them.
func (s *Server) zoneMatrix(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
if err := require(p, auth.ResConfig, false); err != nil {
return err
}
_, st, _, err := s.viewState(w, r)
if err != nil {
return err
}
writeJSON(w, http.StatusOK, matrix.Build(st, matrix.Options{Connected: s.connected(st)}))
return nil
}
// connected maps the networks on the firewall's interfaces (link-local
// and loopback aside) to their zones.
func (s *Server) connected(st *model.State) map[netip.Prefix]string {
out := map[netip.Prefix]string{}
for _, i := range s.hostInterfaces(st) {
if i.Zone == "" {
continue
}
for _, a := range i.Addresses {
p, err := netip.ParsePrefix(a)
if err != nil || p.Addr().IsLinkLocalUnicast() || p.Addr().IsLoopback() {
continue
}
out[p.Masked()] = i.Zone
}
}
return out
}
// ---- the ruleset
func (s *Server) ruleset(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {