Files
abrown38andClaude Opus 5.5 9d766b9823 Boot fallback, flowtable devices that come and go, healthz, renames
The boot unit fails closed: if ruleset.nft doesn't load, it loads
ruleset-fallback.nft (render.Fallback), saved beside it from the same
configuration: nothing new in from external zones, established, the
ICMPv6 IPv6 needs and DHCPv6 replies let in, the inside left alone and
masqueraded, and the unit marked failed with a critical message.
housecarld replaces it when it applies; status, the dashboard and
/healthz say while it is in force. no_external_zone now also says the
fallback closes everything without an external zone.

The boot ruleset has no flowtable: nftables on Debian 13's kernel
refuses one naming a device that doesn't exist yet (ppp0 before PPPoE),
and with it the whole file. internal/linkwatch listens to rtnetlink link
messages; housecarld applies again when a flowtable device appears or
comes back as a new interface (by ifindex), under the engine's lock.
docs/flowtable.md: what it does, when to use it and when not, how.

GET /healthz reports firewall (applied, unconfigured, fallback,
missing), changes on probation and the machine's warnings, with 503
unless in force, read at most every five seconds.

Renaming a zone, set or service changes what names it in the same
change (one history entry, one undo); dynamic sets keep their names.
The importer keeps a chain's head in order: once a would-be built-in
stays a rule, the ones after it do too, so the maintainer's ICMPv6
accepts stay behind the invalid drop as written. hc settings set
--flowtable none empties the list.

make test (nft 1.1.3) and make e2e (242 checks, with VLAN
sub-interfaces, the fallback and the flowtable) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:16:46 +01:00

6.2 KiB

Importing an nftables ruleset

hc import turns the ruleset you have into Housecarl's configuration, and hc takeover deletes the old table once Housecarl's replaces it. Until then both apply.

1. Look first

On the firewall, as root:

sudo hc import --defines /etc/nftables.conf -n

hc runs nft -j list ruleset (and nft -a list ruleset, for the text of rules it can't translate) and sends both to housecarld. -n (dry run) shows what it would make, without saving anything: zones, sets, services, rules, port forwards and NAT, then notes on every guess it made, and the rules it kept raw.

--defines reads your ruleset file's define lines, so sets get your names (define admins = { ... } becomes the set admins). Without it, sets are named after what they hold. Interfaces named by a define name their zone too (define wan_interface = "ppp0" makes the zone wan); otherwise a zone is named after its interface.

From another machine, give the JSON as a file, and the text with --text:

ssh fw 'sudo nft -j list ruleset' > ruleset.json
ssh fw 'sudo nft -a list ruleset' > ruleset.txt
hc import ruleset.json --text ruleset.txt --defines nftables.conf -n

With several tables, --table "inet main" picks one; by default every table but Housecarl's is read. The web UI has the same under Server → Import a ruleset.

What becomes what

  • Interfaces become zones. Zones are marked external when the rules show it: MSS clamping on the way out, DHCPv6 client replies, ping accepted from everywhere else.
  • Sets and defines of addresses become address sets; port lists become services.
  • Jumps and gotos to your own chains are flattened: the jump's condition is added to each rule of the chain, in place.
  • Rules keep their order. A rule that is a built-in (loopback, established, invalid, the ICMPv6 rules, MSS clamping, the DHCPv6 client rule, antispoofing) becomes that built-in only when it sits where the built-in goes, at the head of every chain it covers. Otherwise it stays a rule where it was, so the ruleset does what it did. Since built-ins come before every rule, once one at a chain's head stays a rule, the ones after it there stay rules too, in order: an invalid drop that stays a rule keeps the ICMPv6 accepts after it behind it, as they were. Built-ins your ruleset doesn't have are turned off.
  • DNAT rules become port forwards, TCP and UDP of the same port merged into one. The forward-chain accepts that let them through become part of the port forward, and a masquerade for inside sources to the same host becomes its hairpin. An IPv6 accept to the same port becomes the forward's IPv6 pinhole when there is only one candidate.
  • Maps are expanded, one element at a time. A DNAT map (dnat ip to tcp dport map @forwards, named or written in the rule) becomes one port forward per element; forwards from a named map are called after it (forwards-443). A verdict map (tcp dport vmap { 22 : accept, 23 : drop }) becomes one rule per element, in place, with jumps inlined as above. A lookup matches at most one element, so the rules do the same. A map keyed by several fields (ip saddr . tcp dport) can't be expanded: a rule using a named one is left out with a note, to be added by hand.
  • Masquerade and SNAT become source NAT; redirect becomes a redirect.
  • Meters (update @m { ip saddr limit rate over 3/minute }, add @m { ip saddr ct count over 20 }, quotas in sets) become the rule's per-source (or per-destination) limit, connection limit or quota; a mask in the key (ip saddr and 255.255.255.0) becomes its grouping. Byte rates and quotas are imported too; a quota imported resets only by hand until you give it a period. A rule with a quota or a byte rate that accepts stays a raw rule (rules.md).
  • Policies become the chains' policies.
  • Anything else becomes a raw rule with its original text, in place. A raw rule naming a set of the old table needs the new set name: Housecarl's are NAME_v4 and NAME_v6, and a note says so.

Chains on hooks Housecarl doesn't manage (ingress, a bridge table) are left out, with a note.

2. Import

sudo hc import --defines /etc/nftables.conf
sudo hc ruleset                  # what Housecarl now applies
sudo hc status                   # problems, and the other tables

Housecarl's table is now in force alongside the old one: a packet must be accepted by both. Nothing should change for your traffic. If Housecarl already has a configuration, --replace replaces it.

Imported over the network (not through the local socket), the import is on probation like any change: confirm it.

3. Take over

Once the rules look right (and their hits in the web UI agree), delete the old table:

sudo hc takeover "inet main"

From now on only Housecarl's rules apply. Until you do, the dashboard and hc status warn that the imported table still applies (takeover_pending); they also warn while nftables.service is enabled (nftables_service). Over the network, this is on probation too: unless confirmed, the old table comes back.

4. The boot

Housecarl's ruleset is loaded at boot by housecarl-ruleset.service. Debian's nftables.service loads /etc/nftables.conf, which usually starts with flush ruleset: that would delete Housecarl's table, or bring the old one back. Retire it:

sudo systemctl disable nftables.service
sudo mv /etc/nftables.conf /etc/nftables.conf.before-housecarl

Disable it, but don't stop it (no systemctl stop nftables, no disable --now): Debian's unit stops by running nft flush ruleset, which deletes every table, Housecarl's and Docker's included, at once. Disabled, it simply doesn't run at the next boot.

housecarl-ruleset.service runs after nftables.service in any case, so a leftover flush ruleset can't undo it. Docker, Incus and libvirt keep adding their own tables as before.

Checking the result

  • hc rule list input (and forward) side by side with nft -a list table inet main: the same rules, in the same order, with built-ins above.
  • The notes: each says what to check.
  • hc counters after a while: a rule with no hits that had hits before is worth a look.