Firewall#
Security → Firewall shows every filtering rule the router's configuration
declares, for both address families, with the packet and byte counters nftables has
recorded against each one. It is a view of firewall in the configuration tree joined
with show firewall statistics; it does not read nftables directly and it does not show
the compiled ruleset. Every control on the page stages set and delete commands rather
than applying them, so nothing here changes the router until you commit.
Where the page gets its data#
| What | Call | Refresh |
|---|---|---|
| Rules, chains, default actions, state policy | GET /api/firewall/rules — the whole firewall subtree | on load, and after every commit |
| Hit counters | GET /api/firewall/stats — show firewall statistics | every 5 seconds |
| Groups | GET /api/firewall/groups — the firewall group subtree | on load, and after every commit |
| Where named rulesets are attached | GET /api/interfaces — only when a named ruleset exists in either family | on load |
| The CLI tab | GET /api/config/commands?path=["firewall","<family>"] | when the family changes |
All five are readOnly routes: any authenticated principal may read them
(agent/main.go routes). The interface read is skipped entirely
on a router that filters only through its base chains, which is one fewer call to the
router for the common case — ui/src/pages/Firewall.tsx.
The address family control#
IPv4 and IPv6 are a control, not a constant. The segmented control in the header carries each family's rule count, and it changes what the whole page is looking at: the chain tiles, the ruleset cards, the table, the rule editor's protocol list and the CLI tab all follow it.
That is deliberate, and the reason is in the file's own header comment: a page that shows three empty base chains on a router filtering through named rulesets reads as "this box filters nothing". The empty state says the same thing from the other direction — if the family you are looking at has no rules and the other family has some, the empty state tells you how many the other one has, because a dual-stack router needs both.
The chain tiles#
Four tiles across the top of the page.
| Tile | What it shows |
|---|---|
| input chain | The default action of firewall <family> input filter, and its rule count. Traffic addressed to the router itself. |
| forward chain | The default action of firewall <family> forward filter, and its rule count. Traffic passing through. |
| output chain | The default action of firewall <family> output filter, and its rule count. Traffic the router originates. |
| State policy | Each entry under firewall global-options state-policy, as state → action. |
A default action of drop is shown in the danger colour and anything else in the ok
colour, which reads oddly the first time: on this page red means "this chain is closed".
A chain with no default-action at all shows not set, and that is the one to worry
about — a base chain with no default action and no rules filters nothing.
The state policy tile shows none set when firewall global-options state-policy is
absent. The installer writes three entries when its firewall box is left checked:
set firewall global-options state-policy established action accept
set firewall global-options state-policy related action accept
set firewall global-options state-policy invalid action dropThose are global: they apply before the per-rule state matching described in
rules, which is why almost every rule you write can leave the state
checkboxes clear. The commands are from
packaging/wheelhouse-install.
Named rulesets#
Anything under firewall <family> name <NAME> is a named ruleset. The page treats a named
ruleset and a base chain's filter alike in the table — same columns, same editor — and
additionally gives each named ruleset a card above the table showing its default action,
its rule count, its description, and where it is attached.
The attachment scan walks the interface tree looking for
firewall in name, firewall out name and firewall local name on every interface and
on every vif sub-interface, so a ruleset attached to a VLAN appears as eth1.10 in.
Three states are possible:
| Badge | Meaning |
|---|---|
eth2 in, eth1.10 out, … | The interface tree names this ruleset there. |
| not attached to any interface | The interface tree was read, and nothing referenced this ruleset. It is configuration that filters nothing. |
| attachment not read | The interface read failed. The page says so rather than reporting the ruleset as unattached. |
The distinction in that last row is the point of the badge: "nothing found" and "could not look" are different statements, and only one of them is a problem with your ruleset.
The counters, and what a blank means#
show firewall statistics prints one block per ruleset, separated by a banner of dashes,
with a title such as IPv4 Firewall "input filter" and a table of Rule, Packets,
Bytes rows. The agent splits those blocks into titled sections
(agent/opmode.go splitSections) and the page joins them onto
the configuration rows by family, ruleset title and rule number.
Two details worth knowing when a counter looks wrong:
- Named rulesets have been titled two ways across builds —
name FOOand plainFOO. The page tries both keys rather than losing the counters to a title change. - A failed statistics read blanks the columns; it does not zero them. If
GET /api/firewall/statserrors, the page prints a line saying the counters could not be read and leaves Packets and Bytes empty. Zero packets is an argument for deleting a rule; unknown is not, and the page will not let one look like the other.
Counters come from the running nftables ruleset, so a rule you have staged but not
committed has no counters, and a disabled rule keeps whatever counts it had before it
was unloaded.
The three views#
The segmented control beside the family switch chooses what fills the page below the tiles.
- Rules — the rule table. Documented in rules.
- Groups — the group cards. Documented in groups.
- CLI — the configuration of the selected family, rendered as
setcommands by the router itself:GET /api/config/commandswith the pathfirewall <family>. Read below the admin role it is passed throughredactSecrets, which blanks secret leaf values — a firewall subtree rarely has any, but the wrapper is on the route, not on the page.
The filter box above the table narrows rows by any text they contain, and its count follows the current family.
Integration hints#
Directly under the page header the UI renders any hints installed apps have declared for
the firewall page (GET /api/apps/hints/firewall). Two catalogue entries ship one each,
and both are corrections to a natural mistake:
- Suricata IDS — the IDS raises alerts and drops nothing, so the rules on this page remain the only thing between a flagged flow and its destination.
- WireGuard — a tunnel interface forwards nothing by itself; traffic out of it still meets the forward chain's default action.
Both are in agent/catalog.json under the entry's
integration.hints.
What you need to change anything#
The + Add rule and + Add group buttons are gated on the operator role
(useCanWrite), and staging is gated again on the server: POST /api/stage is
writeable(requireLicense(...)). A viewer sees the whole page and no buttons. An operator
on an unlicensed agent gets a 402 when they try to stage, with the licence state in the
body.
See also#
- Firewall — rules — the table, the editor, and what each field writes.
- Firewall — groups — named sets a rule references instead of a literal.
- Worked rulesets — three complete rulesets, with the commands.
- NAT — translation happens before filtering, and a forward that no filter rule permits is the classic silent failure.
- IDS — what the detection engine does and does not do to a packet.
- A default-drop ruleset — the guide version of the ruleset this page shows you.
- Commands, never hidden — why every panel here
shows its
setlines. - Logs — where a rule's
logmatches appear.
Checked against#
ui/src/pages/Firewall.tsx,
ui/src/components/nav.ts,
ui/src/lib/api.ts,
agent/main.go,
agent/opmode.go,
agent/security.go,
agent/license.go,
agent/catalog.json,
packaging/wheelhouse-install.