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

+75
View File
@@ -206,3 +206,78 @@ func printPacket(w io.Writer, p api.TracePacket) {
fmt.Fprintf(w, " => %s by %s\n", strings.ToUpper(p.Verdict), decided)
}
}
func matrixCmd() *cobra.Command {
return &cobra.Command{
Use: "matrix [FROM TO]", Short: "What the rules do to new connections between zones",
Long: `Shows, for each pair of zones (and the firewall itself), what happens to a
new connection: allowed or blocked, decided by a rule or the policy, and
how many rules decide for part of the traffic first (+N: exceptions,
such as some services or addresses). With FROM and TO (a zone, or
"firewall"), that pair's exceptions in order, and what decides the rest.
hc matrix
hc matrix guest wan
hc matrix lan firewall`,
Args: cobra.RangeArgs(0, 2),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
return fmt.Errorf("give both: FROM and TO")
}
m, err := get[api.Matrix]("/matrix", nil)
if err != nil {
return err
}
if len(args) == 2 {
for _, c := range m.Cells {
if strings.EqualFold(c.From, args[0]) && strings.EqualFold(c.To, args[1]) {
return output(c, func(w io.Writer) { printCell(w, c) })
}
}
return fmt.Errorf("no pair %s → %s (zones: %s, and firewall)", args[0], args[1], strings.Join(m.Zones, ", "))
}
return output(m, func(w io.Writer) { printMatrix(w, m) })
},
}
}
func printMatrix(w io.Writer, m api.Matrix) {
cols := append(append([]string{}, m.Zones...), "firewall")
cells := map[[2]string]api.MatrixCell{}
for _, c := range m.Cells {
cells[[2]string{c.From, c.To}] = c
}
color := isTerminal(os.Stdout) && flagOutput == "table"
fmt.Fprint(w, "FROM \\ TO")
for _, c := range cols {
fmt.Fprintf(w, "\t%s", c)
}
fmt.Fprintln(w)
for _, from := range cols {
fmt.Fprint(w, from)
for _, to := range cols {
c, ok := cells[[2]string{from, to}]
text := "-"
if ok {
text = map[string]string{"allowed": "allow", "blocked": "block"}[c.Verdict]
if n := len(c.Exceptions); n > 0 {
text += fmt.Sprintf(" +%d", n)
}
if color {
text = map[string]string{"allowed": "\x1b[32m", "blocked": "\x1b[31m"}[c.Verdict] + text + "\x1b[0m"
}
}
fmt.Fprintf(w, "\t%s", text)
}
fmt.Fprintln(w)
}
fmt.Fprintln(w, "\nallow/block: new connections no exception covers; +N: rules deciding for part of the traffic first ('hc matrix FROM TO')")
}
func printCell(w io.Writer, c api.MatrixCell) {
fmt.Fprintf(w, "%s → %s (the %s chain), new connections:\n", c.From, c.To, c.Chain)
for _, e := range c.Exceptions {
fmt.Fprintf(w, " %s\t%s\t%s\n", e.Verdict, e.What, e.Label)
}
fmt.Fprintf(w, " %s\tthe rest\t%s\n", c.Verdict, c.DecidedBy)
}