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

+3
View File
@@ -5,6 +5,7 @@ import { api, get, state, can, setUnauthorizedHandler, setChangeHandler, loadDra
import * as changes from "./pages/changes.js";
import * as connections from "./pages/connections.js";
import * as tracePage from "./pages/trace.js";
import * as matrixPage from "./pages/matrix.js";
import * as dashboard from "./pages/dashboard.js";
import * as rules from "./pages/rules.js";
import * as nat from "./pages/nat.js";
@@ -24,6 +25,7 @@ const routes = [
{ pattern: /^$/, page: dashboard, nav: "dashboard" },
{ pattern: /^rules$/, page: rules, nav: "rules" },
{ pattern: /^nat$/, page: nat, nav: "nat" },
{ pattern: /^matrix$/, page: matrixPage, nav: "matrix" },
{ pattern: /^zones$/, page: zones, nav: "zones" },
{ pattern: /^sets$/, page: sets, nav: "sets" },
{ pattern: /^services$/, page: services, nav: "services" },
@@ -45,6 +47,7 @@ const routes = [
const navItems = [
["dashboard", "#/", "Dashboard"],
["rules", "#/rules", "Rules", "config"],
["matrix", "#/matrix", "Matrix", "config"],
["nat", "#/nat", "NAT", "config"],
{ menu: "Objects", items: [
["zones", "#/zones", "Zones", "config"],
@@ -0,0 +1,38 @@
// Matrix: what the rules do to new connections between zones, and to and
// from the firewall itself.
import { h, mount, modal, badge } from "../ui.js";
import { get, state } from "../api.js";
export async function render(el) {
const m = await get("/matrix");
const cols = [...m.zones, "firewall"];
const cells = new Map(m.cells.map((c) => [c.from + "\u0000" + c.to, c]));
const pending = state.draft && !state.immediate && (state.draft.ops || []).length;
const name = (z) => z === "firewall" ? h("em", null, "firewall") : z;
mount(el,
h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Zone matrix"),
h("div", { class: "muted" }, "What happens to a new connection from a zone (rows) to a zone (columns), or to and from the firewall itself, worked out from the rules in order: allowed or blocked, and how many rules decide for part of the traffic first (some services, addresses, times). Click a cell for the rules."))),
pending ? h("div", { class: "msg info" }, "Shown with your pending changes.") : null,
!m.zones.length ? h("div", { class: "panel" }, h("div", { class: "empty" }, "No zones yet: add them under Objects → Zones.")) :
h("div", { class: "panel" }, h("div", { class: "table-wrap" }, h("table", { class: "matrix" },
h("thead", null, h("tr", null, h("th", { class: "corner" }, "from ↓ to →"), cols.map((c) => h("th", null, name(c))))),
h("tbody", null, cols.map((from) => h("tr", null, h("th", null, name(from)), cols.map((to) => {
const c = cells.get(from + "\u0000" + to);
if (!c) return h("td", { class: "matrix-none" }, "–");
const n = c.exceptions.length;
return h("td", { class: "matrix-" + c.verdict + (n ? " matrix-some" : ""), title: c.verdict + " by " + c.decided_by + (n ? ", " + n + " exception" + (n === 1 ? "" : "s") + " first" : "") },
h("button", { type: "button", class: "matrix-cell", onclick: () => detail(c) }, c.verdict, n ? h("span", { class: "matrix-n" }, "+" + n) : null));
})))))),
h("p", { class: "muted small" }, "Zone to zone is the forward chain; to the firewall, input; from it, output. Exceptions are rules (and built-ins such as ping or port forwards) that decide for some of the traffic before the rest falls to the deciding rule or the policy. Replies and established connections aren't shown: the built-in lets those through.")));
}
function detail(c) {
const ruleLink = (id, chain, label) => id ? h("a", { href: "#/rules?chain=" + chain, onclick: () => document.querySelector(".modal-back") && document.querySelector(".modal-back").remove() }, label) : label;
const v = (verdict) => badge(verdict, { allowed: "ok", blocked: "err", logged: "", unknown: "warn" }[verdict]);
modal(c.from + " → " + c.to, h("div", null,
h("p", { class: "muted" }, "New connections, checked in this order (the " + c.chain + " chain):"),
h("div", { class: "table-wrap" }, h("table", null, h("tbody", null,
c.exceptions.map((e) => h("tr", null, h("td", null, v(e.verdict)), h("td", null, e.what), h("td", { class: "small" }, ruleLink(e.rule_id, e.chain, e.label)))),
h("tr", { class: "matrix-rest" }, h("td", null, v(c.verdict)), h("td", null, h("strong", null, c.exceptions.length ? "the rest" : "everything")),
h("td", { class: "small" }, ruleLink(c.rule_id, c.chain, c.decided_by))))))), [{ label: "Close", value: null }], { wide: true });
}
+11
View File
@@ -419,3 +419,14 @@ table.trace-steps { width: 100%; margin-top: 6px; }
table.trace-steps td { padding: 3px 8px; border-top: 1px solid var(--border); }
tr.step-drop td { background: var(--del); }
tr.step-accept td { background: var(--add); }
/* The zone matrix: a cell per pair, coloured by what happens. */
table.matrix th, table.matrix td { text-align: center; }
table.matrix tbody th { text-align: left; }
table.matrix th.corner { font-weight: 400; color: var(--muted); text-transform: none; }
table.matrix td { padding: 4px; }
.matrix-cell { width: 100%; min-width: 7em; border: 0; background: transparent; color: inherit; font-weight: 600; padding: 6px 8px; cursor: pointer; }
td.matrix-allowed { background: var(--add); color: var(--add-text); }
td.matrix-blocked { background: var(--del); color: var(--del-text); }
.matrix-n { margin-left: 6px; font-weight: 400; font-size: 12px; color: var(--warn); }
td.matrix-none { color: var(--muted); }
tr.matrix-rest td { border-top: 2px solid var(--border); }