Wheelhouse docs

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#

WhatCallRefresh
Rules, chains, default actions, state policyGET /api/firewall/rules — the whole firewall subtreeon load, and after every commit
Hit countersGET /api/firewall/statsshow firewall statisticsevery 5 seconds
GroupsGET /api/firewall/groups — the firewall group subtreeon load, and after every commit
Where named rulesets are attachedGET /api/interfacesonly when a named ruleset exists in either familyon load
The CLI tabGET /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.

TileWhat it shows
input chainThe default action of firewall <family> input filter, and its rule count. Traffic addressed to the router itself.
forward chainThe default action of firewall <family> forward filter, and its rule count. Traffic passing through.
output chainThe default action of firewall <family> output filter, and its rule count. Traffic the router originates.
State policyEach 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 drop

Those 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:

BadgeMeaning
eth2 in, eth1.10 out, …The interface tree names this ruleset there.
not attached to any interfaceThe interface tree was read, and nothing referenced this ruleset. It is configuration that filters nothing.
attachment not readThe 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:

  1. Named rulesets have been titled two ways across builds — name FOO and plain FOO. The page tries both keys rather than losing the counters to a title change.
  2. A failed statistics read blanks the columns; it does not zero them. If GET /api/firewall/stats errors, 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 set commands by the router itself: GET /api/config/commands with the path firewall <family>. Read below the admin role it is passed through redactSecrets, 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 set lines.
  • Logs — where a rule's log matches 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.

Updated 2026-09-02 manual firewall nftables ipv6