Zone matrix: partly, what is let through, hits; hc matrix aligned

A cell reads allow, block or partly (some new connections let through,
the rest not), with its exceptions counted by what they do (+3 -1), and
what they let through: services with the addresses they are limited to,
under the grid in hc matrix and in each cell on the Matrix page. A zone
to itself is marked: only traffic routed between its interfaces passes
the firewall. The details list each rule's hits. hc matrix pads the
grid by visible width: the colours' escape codes had thrown it out of
line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-20 19:00:33 +01:00
1 parent 429c910df1
commit 5ee324c588
14 files changed
+423 -65

No files matched your search

+102 -14
View File
@@ -63,6 +63,50 @@ func Cell(st *model.State, o Options, from, to string) (api.MatrixCell, bool) {
}
func cell(st *model.State, o Options, chain, from, to string) api.MatrixCell {
c := walkCell(st, o, chain, from, to)
sum(&c)
if from == to && chain == model.ChainForward {
c.SameZone, c.Note = true, sameZoneNote(st.Zone(from))
}
return c
}
// sum fills in the overview: the summary, the exceptions counted by
// verdict, and what the allowing ones let through.
func sum(c *api.MatrixCell) {
c.Open = []string{}
seen := map[string]bool{}
for _, e := range c.Exceptions {
switch e.Verdict {
case "allowed":
c.Allowed++
if !seen[e.Short] {
seen[e.Short] = true
c.Open = append(c.Open, e.Short)
}
case "blocked":
c.Blocked++
}
}
c.Summary = c.Verdict
if c.Verdict == "blocked" && c.Allowed > 0 || c.Verdict == "allowed" && c.Blocked > 0 {
c.Summary = "partly"
}
}
// sameZoneNote says what of a zone's traffic to itself the firewall sees:
// only what it routes between the zone's interfaces.
func sameZoneNote(z *model.Zone) string {
if z == nil {
return ""
}
if len(z.Interfaces) == 1 && !strings.HasSuffix(z.Interfaces[0], "*") {
return fmt.Sprintf("traffic within %s stays on its own network: it passes the firewall only if routed back out of %s", z.Name, z.Interfaces[0])
}
return fmt.Sprintf("only traffic routed between %s's interfaces (%s) passes the firewall; within one of them it stays on its own network", z.Name, strings.Join(z.Interfaces, ", "))
}
func walkCell(st *model.State, o Options, chain, from, to string) api.MatrixCell {
c := api.MatrixCell{From: from, To: to, Chain: chain, Exceptions: []api.MatrixException{}}
b := st.Settings.Builtins
fromZone := st.Zone(from)
@@ -71,7 +115,7 @@ func cell(st *model.State, o Options, chain, from, to string) api.MatrixCell {
if chain != model.ChainOutput {
if b.Antispoof && external {
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: model.ChainEarly, Label: "built-in: antispoof", Verdict: "blocked",
What: "sources that can't come from outside (private ranges, your own prefixes)"})
What: "sources that can't come from outside (private ranges, your own prefixes)", Short: "spoofed sources", Marks: []string{"b-antispoof", "b-antispoof6"}})
}
if decided := walk(st, o, &c, model.ChainEarly, from, ""); decided {
return c
@@ -81,22 +125,26 @@ func cell(st *model.State, o Options, chain, from, to string) api.MatrixCell {
switch chain {
case model.ChainInput:
if b.ICMPv6 {
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: icmpv6", Verdict: "allowed", What: "ICMPv6 errors, echo, neighbour discovery, MLD"})
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: icmpv6", Verdict: "allowed", What: "ICMPv6 errors, echo, neighbour discovery, MLD",
Short: "ICMPv6", Marks: []string{"b-icmpv6", "b-icmpv6-nd", "b-icmpv6-mld"}})
}
if b.PingInternal && !external {
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: ping", Verdict: "allowed", What: "ping (ICMP echo)"})
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: ping", Verdict: "allowed", What: "ping (ICMP echo)", Short: "ping", Marks: []string{"b-ping"}})
}
if b.DHCPv6Client && external {
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: dhcpv6-client", Verdict: "allowed", What: "DHCPv6 replies to the firewall"})
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: dhcpv6-client", Verdict: "allowed", What: "DHCPv6 replies to the firewall",
Short: "DHCPv6", Marks: []string{"b-dhcpv6-client"}})
}
case model.ChainForward:
if n := forwards(st); n > 0 && external && !st.Zone(to).External {
if marks := forwards(st); len(marks) > 0 && external && !st.Zone(to).External {
n := fmt.Sprintf("%d port forward%s", len(marks), map[bool]string{true: "", false: "s"}[len(marks) == 1])
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "port forwards", Verdict: "allowed",
What: fmt.Sprintf("%d port forward%s to inside hosts", n, map[bool]string{true: "", false: "s"}[n == 1])})
What: n + " to inside hosts", Short: n, Marks: marks})
}
case model.ChainOutput:
if st.Settings.OutputPolicy == model.PolicyDrop && b.ICMPv6 {
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: icmpv6", Verdict: "allowed", What: "ICMPv6 errors, echo, neighbour discovery, MLD"})
c.Exceptions = append(c.Exceptions, api.MatrixException{Chain: chain, Label: "built-in: icmpv6", Verdict: "allowed", What: "ICMPv6 errors, echo, neighbour discovery, MLD",
Short: "ICMPv6", Marks: []string{"b-icmpv6"}})
}
}
if walk(st, o, &c, chain, from, to) {
@@ -104,17 +152,22 @@ func cell(st *model.State, o Options, chain, from, to string) api.MatrixCell {
}
policy := map[string]string{model.ChainInput: st.Settings.InputPolicy, model.ChainForward: st.Settings.ForwardPolicy, model.ChainOutput: st.Settings.OutputPolicy}[chain]
c.Verdict, c.DecidedBy = verdictOf(policy), chain+" policy"
if policy == model.PolicyDrop {
c.Marks = []string{"p-" + chain} // only a drop policy is counted
}
return c
}
func forwards(st *model.State) int {
n := 0
// forwards returns the counters' marks of the port forwards that send
// connections inside.
func forwards(st *model.State) []string {
var marks []string
for _, f := range st.Forwards {
if !f.Disabled && f.To != "" {
n++
marks = append(marks, fmt.Sprintf("f%d", f.ID))
}
}
return n
return marks
}
// walk goes through a chain's rules for the pair, as nftables checks
@@ -130,8 +183,10 @@ func walk(st *model.State, o Options, c *api.MatrixCell, chain, from, to string)
}
}
for _, r := range rules {
mark := []string{fmt.Sprintf("r%d", r.ID)}
if r.Raw != "" {
c.Exceptions = append(c.Exceptions, api.MatrixException{RuleID: r.ID, Chain: chain, Label: "rule " + r.Label(), Verdict: "unknown", What: "a raw rule: " + r.Raw})
c.Exceptions = append(c.Exceptions, api.MatrixException{RuleID: r.ID, Chain: chain, Label: "rule " + r.Label(), Verdict: "unknown", What: "a raw rule: " + r.Raw,
Short: "a raw rule", Marks: mark})
continue
}
if !zones(r, from, to) || !forNew(r) {
@@ -151,10 +206,11 @@ func walk(st *model.State, o Options, c *api.MatrixCell, chain, from, to string)
if what == "" {
what = "everything"
}
c.Exceptions = append(c.Exceptions, api.MatrixException{RuleID: r.ID, Chain: chain, Label: "rule " + r.Label(), Verdict: v, What: what})
c.Exceptions = append(c.Exceptions, api.MatrixException{RuleID: r.ID, Chain: chain, Label: "rule " + r.Label(), Verdict: v, What: what,
Short: short(r, what), Marks: mark})
continue
}
c.Verdict, c.DecidedBy, c.RuleID = v, "rule "+r.Label(), r.ID
c.Verdict, c.DecidedBy, c.RuleID, c.Marks = v, "rule "+r.Label(), r.ID, mark
return true
}
return false
@@ -265,6 +321,38 @@ func forNew(r *model.Rule) bool {
return false
}
// short sums up an exception for the overview: its services (ICMP types
// as just icmp or icmpv6), with the addresses it is limited to, which
// matter as much ("tcp/22 from trusted"); times and rates are left to
// the details.
func short(r *model.Rule, what string) string {
var parts []string
if len(r.Services) > 0 {
var svcs []string
seen := map[string]bool{}
for _, s := range r.Services {
if p, _, ok := strings.Cut(s, "/"); ok && (p == "icmp" || p == "icmpv6") {
s = p
}
if !seen[s] {
seen[s] = true
svcs = append(svcs, s)
}
}
parts = append(parts, strings.Join(svcs, ", "))
}
for _, c := range strings.Split(what, " · ") {
if strings.HasPrefix(c, "from ") || strings.HasPrefix(c, "to ") {
parts = append(parts, c)
}
}
if len(parts) == 0 {
first, _, _ := strings.Cut(what, " · ")
return first
}
return strings.Join(parts, " ")
}
// conditions says what limits a rule to part of a pair's traffic ("" for
// none: it decides the pair).
func conditions(st *model.State, r *model.Rule) string {