Files
housecarl/docs/nat.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

3.2 KiB

NAT

Port forwards

A port forward publishes an inside host's service: connections to a public port are sent to the host, and let through the forward chain. One object does what takes three rules by hand (the DNAT, the forward accept, and the hairpin masquerade).

hc forward add 8096 --to 10.0.10.57:443 --public 203.0.113.7 --hairpin --name jellyfin
hc forward add 7777-7780 --proto tcp,udp --to 10.0.10.76 --public 203.0.113.7 --hairpin --name terraria
hc forward add 25 --to 10.0.10.25 --from mail-relays --name smtp
hc forward list
PORTS the public port or range
--proto tcp, udp or both (tcp,udp); tcp by default
--to HOST[:PORT] the inside IPv4 host, and its port if it differs. A range keeps its ports
--public the public IPv4 addresses (or a set) to match. Without it, any address arriving on an external zone
--from only these sources may connect
--hairpin the public address also works from inside (below); --hairpin-from limits that to some inside sources
--to6 an inside IPv6 host to open the port to (below)

In the ruleset, a forward is a dnat ip to rule in the dstnat chain, an accept in forward for connections it translated (ct status dnat), placed after the built-ins and before your forward rules, so a later "drop everything from wan" rule doesn't stop it.

Hairpin NAT

From inside, a client that connects to the public address (because DNS gives it) has its connection sent to the inside host, which would answer it directly, and the client would drop the answer. With --hairpin, Housecarl also masquerades those connections, so the answers come back through the firewall. It needs --public: without the public address, there is nothing to recognise those connections by.

IPv6

IPv6 needs no translation: inside hosts have public addresses. --to6 opens the same port (or the --to port, if given) to an IPv6 host:

hc forward add 8096 --to 10.0.10.57:443 --to6 2001:db8:7f3d:10::57 --public 203.0.113.7 --name jellyfin

IPv6 clients connect to [2001:db8:7f3d:10::57]:443 directly. A forward can have only --to6, to open an IPv6 port alone.

Source NAT

Source NAT rewrites the source of traffic leaving the firewall: masquerade (the outgoing interface's address) or a fixed address with --snat-to. Like rules, they are in order and match by zone, address and service; the first that matches decides.

hc snat add --out wan --from trusted --comment "the LAN to the internet"
hc snat add --out wg0 --not-out --from wg-clients
hc snat add --out wan --from fd00:abab::/48 --family ipv6 --snat-to 2001:db8:7f3d::1

Source NAT is IPv4 only unless --family ipv6 says otherwise: with a delegated prefix, IPv6 needs no NAT, and NAT66 is for unusual cases (a ULA network behind one public address).

Redirects

A redirect sends matching traffic to a port on the firewall itself, for example to make every client use the firewall's DNS server:

hc redirect add --in lan --service dns --not-to 10.0.10.5 --comment "force DNS"
hc redirect add --in lan --service http --to-port 3128

Without --to-port, the service's own port. The input chain must accept the redirected traffic: redirected DNS needs a rule accepting DNS from lan.