Files
housecarl/docs/import.md
T
abrown38andClaude Opus 5.5 ebec2cfcca Import nftables maps: one port forward or rule per element
A DNAT map (named or anonymous) becomes one port forward per element,
named after the map; a verdict map becomes one rule per element, in
place, with jumps inlined. A lookup matches one element at most, so the
expanded rules do the same. A named map that can't be expanded (keyed by
several fields) is left out with a note instead of a raw rule naming it,
which nftables would refuse. Test fixture made with nft 1.1.3.

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

130 lines
5.1 KiB
Markdown

# 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:
```sh
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 trusted_ips = { ... }` becomes the set `trusted_ips`).
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`:
```sh
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. 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 `jump`s 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.
* **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
```sh
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:
```sh
sudo hc takeover "inet main"
```
From now on only Housecarl's rules apply. 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:
```sh
sudo systemctl disable nftables.service
sudo mv /etc/nftables.conf /etc/nftables.conf.before-housecarl
```
`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.