Files
housecarl/docs/rules.md
T
abrown38andClaude Opus 5.5 34292eeebb Build Housecarl 0.1.0: the nftables firewall manager
Zones, address sets (IPv4 and IPv6 mixed, dynamic sets with timeouts) and
services, ordered rules in the early, input, forward and output chains,
port forwards with hairpin NAT and IPv6 pinholes, source NAT, redirects,
and switchable built-ins (loopback, conntrack, ICMPv6, antispoofing, MSS
clamping, DHCPv6 client, flowtable). Every change is validated, checked
with nft -c and applied in one transaction to Housecarl's own table,
inet housecarl; changes made over the network roll back unless
confirmed, and the confirmed ruleset is loaded at boot by
housecarl-ruleset.service. An empty configuration applies nothing.

Also: hit counters and the drop log (nflog), 'hc import' of an existing
nftables ruleset and 'hc takeover', the hc CLI, the REST API and the web
UI, users, scoped tokens (sets:write:NAME), the audit log, the web UI's
certificate through Jarl, Debian packages, docs, unit tests with real
nft 1.1.3, and an e2e test with real traffic (71 checks).

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

8.5 KiB

Zones, sets, services and rules

Zones

A zone names one or more interfaces, so rules can say "from the LAN" rather than enp2s0. An interface belongs to one zone at most. A trailing * matches many: incusbr*.

hc zone add wan --interface wan --external
hc zone add lan --interface enp2s0 --interface 'enp2s0.*'
hc zone add wg  --interface wg0

External zones face the internet. Antispoofing, MSS clamping, the DHCPv6 client rule and port forwards (without public addresses) apply on them. Everything else is internal: ping to the firewall from internal zones is accepted by a built-in.

A zone may name an interface that doesn't exist yet (a WireGuard interface that comes up later): its rules match once it appears, and hc status warns until then.

Address sets

A set is a named list of IPv4 and IPv6 addresses, prefixes and ranges, mixed:

hc set add trusted 10.0.10.0/24 10.20.30.0/24 2001:db8:7f3d:10::/64
hc set add dns-servers 10.0.10.5 2001:db8:7f3d:10::5 10.0.10.250-10.0.10.254

In the ruleset each becomes two nftables sets, trusted_v4 and trusted_v6 (one if the set has only one family). A rule that names it becomes one nftables rule per family. Rules can also give addresses directly (--from 10.0.10.3); a set is better when several rules share them.

A dynamic set's entries are added at run time, each with a timeout, and nftables removes them when it runs out, even with housecarld stopped:

hc set add guests --dynamic --timeout 24h
hc set entries add guests 10.0.30.17                 # for 24 hours
hc set entries add guests 2001:db8:7f3d:30::17 --timeout 2h
hc set entries guests                                # with the time left
hc set entries remove guests 10.0.30.17

Entries aren't configuration: they aren't in the history and aren't on probation, but each change is in the audit log. They survive every change to the ruleset, and a restart of housecarld; a reboot empties them. A token can be limited to one set (permissions.md): that is how another program (Steward's captive portal) lets a client in.

Services

A service names ports of a protocol (tcp, udp, sctp) and ICMP types:

hc service add jellyfin tcp/8096 tcp/8920
hc service add games udp/27015-27030 tcp/27015
hc service add pmtu icmp/destination-unreachable icmpv6/packet-too-big

Many are built in (hc service list): ssh, dns (TCP and UDP 53), dot, http, https (TCP, and UDP for QUIC), smtp, submission, smtps, imaps, ntp, dhcp, dhcpv6, wireguard, openvpn, ipsec, snmp, syslog, mdns, rdp, and ping (ICMP and ICMPv6 echo requests). Adding a service with a built-in's name replaces it. Rules may also give an entry directly: --service tcp/8080.

Rules

Rules are kept in order, in four chains:

Chain Hook
input traffic to the firewall itself input
forward traffic through it, between zones forward
output traffic from it output
early before connection tracking: cheap drops of what can never be right prerouting, priority raw

Each chain is checked top to bottom, and the first rule that accepts, drops or rejects decides. What no rule decides gets the chain's policy (Settings): by default input and forward drop, output accepts.

hc rule add input --from trusted --service ssh --comment "SSH from home"
hc rule add input --in wg --service dns
hc rule add forward --in lan --out wan
hc rule add forward --in wan -a drop --comment "nothing else gets in"
hc rule list input
hc rule move 12 --before 7

A rule matches on what it gives, and all of it:

--in, --out zones it comes in on, goes out on (--not-in, --not-out: any other). Input has no out zone, output no in zone
--from, --to sets, addresses, prefixes, ranges (--not-from, --not-to: anything else)
--service services or entries (tcp/8080, icmp/echo-request)
--family ipv4 or ipv6 only; otherwise it follows the addresses
--state connection states: new, established, related, invalid, untracked
--status connection status: dnat (port forwarded), snat
--days, --hours a time window: --days Monday,Friday --hours 08:00-17:30; 22:00-06:00 passes midnight. In the firewall's time zone, as the kernel knows it
--limit a rate: 10/minute, with ,burst=N, ,over (match beyond the rate, to drop a flood) and ,per-source (a rate for each source address)
--conn-limit N a source with more than N connections

The action is accept, drop, reject (an ICMP error, or with --reject-with tcp-reset a TCP reset) or log (log and go on). --log logs the packets of any rule (operations.md). --disabled (or hc rule disable ID) keeps a rule without using it.

Examples:

# SSH from the internet, at most 3 new connections a minute per source
hc rule add input --in wan --service ssh --state new --limit 3/minute,burst=5,over,per-source -a drop --log
hc rule add input --in wan --service ssh

# Children's devices offline at night
hc rule add forward --from kids --out wan --hours 22:00-07:00 -a reject

# What was port forwarded may go on, the rest from the internet may not
hc rule add forward --in wan --status dnat
hc rule add forward --in wan -a drop

Raw rules

For what Housecarl can't say, a rule can be one nftables rule written by hand, placed like any other:

hc rule add forward --raw 'iifname != "wan" ct state invalid ip daddr @trusted_v4 accept'

It is checked by nftables with the rest of the ruleset, and can use the sets' nftables names (@trusted_v4). The importer makes raw rules from what it can't translate. It goes into the ruleset as written: add counter to it to count its packets.

Built-ins

Housecarl adds the rules every firewall needs before yours. Each can be turned off (hc settings set --builtin NAME=off; Settings in the web UI), and hc ruleset shows them:

Built-in Default Chains What
loopback on input (and output if its policy is drop) accept everything on lo
established on input, forward (and output) accept established and related connections
drop_invalid on input, forward drop packets connection tracking calls invalid
icmpv6 on input, forward (and output) the ICMPv6 IPv6 can't work without (RFC 4890): errors and echo; neighbour discovery with hop limit 255 and MLD from link-local, to the firewall
ping_internal on input ping (ICMP echo) to the firewall from internal zones
dhcpv6_client on input DHCPv6 replies from link-local on external zones, so the firewall gets its prefix delegation
antispoof on early drop sources that can't come from the internet, on external zones: the antispoof lists (private, loopback, documentation and multicast ranges) and your own prefixes
mss_clamp off forward clamp TCP's MSS to the route's MTU on external zones: needed with PPPoE

Your prefixes (--own-prefix 2001:db8:7f3d::/48) are your delegated IPv6 prefix and public addresses: nothing from outside should come from them. The antispoof lists can be changed with --antispoof-v4 and --antispoof-v6.

The flowtable (--flowtable wan --flowtable enp2s0) lets established TCP and UDP connections forwarded between those devices skip the rules, which helps on a slow CPU. It needs exact device names. Counters of forward rules then only count each connection's first packets.

The ruleset

hc ruleset prints what Housecarl builds; the web UI and hc rule list show each rule's hits. The table looks like this (shortened):

table inet housecarl {
	set trusted_v4 { type ipv4_addr; flags interval; elements = { 10.0.10.0/24, 10.20.30.0/24 } }
	set trusted_v6 { type ipv6_addr; flags interval; elements = { 2001:db8:7f3d:10::/64 } }
	chain early   { type filter hook prerouting priority raw; policy accept; ... }
	chain input   { type filter hook input priority filter; policy drop;
		iifname "lo" accept comment "hc:b-loopback"
		ct state established,related counter accept comment "hc:b-established"
		...
		ip saddr @trusted_v4 tcp dport 22 counter accept comment "hc:r4 SSH from home"
		ip6 saddr @trusted_v6 tcp dport 22 counter accept comment "hc:r4 SSH from home"
		counter comment "hc:p-input"
	}
	chain forward { ... }
	chain output  { ... }
	chain dstnat  { type nat hook prerouting priority dstnat; ... }
	chain srcnat  { type nat hook postrouting priority srcnat; ... }
}

Other tables still apply: a packet must get past every table's chains on its hook. The dashboard and hc status list them.