- Undo is staged like any edit: in review mode the web UI's Undo, and hc history undo with -C or HOUSECARL_CHANGESET, add a revert operation that checks, when replayed, that nothing it touches changed since; applied, History links the entries. Imports and takeovers still can't be staged. - The rules page sums up a chain's built-ins in one line; a window lists them with what they do and their hits, and switches them. - Warnings about the machine (dashboard, hc status): nftables.service enabled (it loads /etc/nftables.conf at boot and flushes every table when stopped), and the imported table still in force beside Housecarl's. The docs say to disable nftables.service but not stop it. - A raw rule with other fields set is refused (raw_conflict) instead of silently ignoring them; the rule dialog's nftables section hides what it replaces, and the importer's raw rules carry only their text. - A dynamic set with configured entries is refused (set_dynamic_entries): they were silently ignored. The set dialog shows entries or the entry timeout, as fits. - The rule dialog's chain choices are just the chains' names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
12 KiB
Markdown
261 lines
12 KiB
Markdown
# Zones, sets, services and rules
|
|
|
|
## Zones
|
|
|
|
A zone names one or more interfaces, so rules can say "from the LAN"
|
|
rather than `eth1`. An interface belongs to one zone at most. A trailing
|
|
`*` matches many: `incusbr*`.
|
|
|
|
```sh
|
|
hc zone add wan --interface wan --external
|
|
hc zone add lan --interface eth1 --interface 'eth1.*'
|
|
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:
|
|
|
|
```sh
|
|
hc set add trusted 192.168.1.0/24 192.168.2.0/24 2001:db8:1::/64
|
|
hc set add dns-servers 192.168.1.5 2001:db8:1::5 192.168.1.250-192.168.1.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 192.168.1.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.
|
|
It has no configured entries (validation refuses them); its timeout is
|
|
the default for entries added without one, and without a timeout they
|
|
stay until removed:
|
|
|
|
```sh
|
|
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:3::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](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:
|
|
|
|
```sh
|
|
hc service add web tcp/8080 tcp/8443
|
|
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.
|
|
|
|
```sh
|
|
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`, or bytes: `1mbytes/second`; with `,burst=N`, `,over` (match beyond the rate) and `,per-source` or `,per-destination` ([Bandwidth and quotas](#bandwidth-and-quotas)) |
|
|
| `--quota` | an allowance of traffic: `50gbytes,over,per-source,monthly=15` ([Bandwidth and quotas](#bandwidth-and-quotas)) |
|
|
| `--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](operations.md#the-log)).
|
|
`--disabled` (or `hc rule disable ID`) keeps a rule without using it.
|
|
|
|
Examples:
|
|
|
|
```sh
|
|
# 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
|
|
```
|
|
|
|
### Bandwidth and quotas
|
|
|
|
A **byte rate** limits bandwidth: `--limit 20mbytes/second,over,per-source
|
|
-a drop` drops what a device sends beyond 20 MB/s. Units are `bytes`,
|
|
`kbytes` and `mbytes` (`kb`, `mb` for short); the burst is in the same
|
|
unit. nftables can only hold byte rates per second (up to 4 GB/s) or per
|
|
minute (up to 293 MB/minute); validation says so for larger ones. This is
|
|
policing: traffic beyond the rate is dropped, not queued, so TCP slows to
|
|
fit, and UDP loses packets. Shaping (queues, fair shares) is `tc`'s work,
|
|
not a firewall's.
|
|
|
|
A **quota** is an allowance of traffic. `over` matches once it is used up,
|
|
to drop what comes after:
|
|
|
|
```sh
|
|
# Each device on the LAN: 50 GB a month, from the 15th (a billing day)
|
|
hc rule add forward --in lan --out wan --quota 50gbytes,over,per-source,monthly=15 -a drop
|
|
# The whole guest network: 10 GB a day together
|
|
hc rule add forward --in guest --out wan --quota 10gbytes,over,daily -a drop
|
|
hc quotas # what each used, and the next reset
|
|
hc rule reset-quota 31 # start again now
|
|
```
|
|
|
|
| | |
|
|
|---|---|
|
|
| `per-source`, `per-destination` | each address its own allowance; without, one allowance for everything the rule matches |
|
|
| `daily`, `weekly`, `monthly=DAY` | when it starts again: at midnight; weekly on Mondays; monthly on that day (the last day of a shorter month). Without one, only by hand |
|
|
|
|
nftables counts a quota but never resets it, and forgets it when its
|
|
table is replaced: Housecarl carries what each quota used across every
|
|
change and restart, saves it every five minutes (and when housecarld
|
|
stops) for after a reboot, and resets quotas when their period ends.
|
|
A per-address quota counts an address: a device that gets a new one
|
|
starts again. With Steward's reservations IPv4 addresses stay; IPv6 is
|
|
grouped by /64 (below).
|
|
|
|
**Rules with a quota or a byte rate count every packet.** The built-in
|
|
that accepts established connections comes before your rules, so a rule
|
|
there only sees each connection's first packet. Rules that count traffic
|
|
are checked before it instead, in their order (the web UI lists them
|
|
first, marked "every packet"). So they can drop, reject or log, but not
|
|
accept: an accept there would let packets past every later rule. Let the
|
|
traffic in with a rule of its own. The flowtable's fast path skips the
|
|
forward chain for established connections, so with it on, forward
|
|
quotas and byte rates only count first packets (a warning says so).
|
|
|
|
**Per-address limits group addresses**: each IPv4 address on its own, and
|
|
IPv6 addresses by /64, since one device uses several (privacy addresses
|
|
change). `--group-v4 24` counts each /24 together; `--group-v6 128` each
|
|
IPv6 address alone. This applies to per-address rates, connection limits
|
|
and quotas.
|
|
|
|
### Raw rules
|
|
|
|
For what Housecarl can't say, a rule can be one nftables rule written by
|
|
hand, placed like any other:
|
|
|
|
```sh
|
|
hc rule add forward --raw 'iifname != "wan" ct state invalid ip daddr @trusted_v4 accept'
|
|
```
|
|
|
|
It is used exactly as written: its text replaces the rule's zones,
|
|
addresses, services, action, limits and log, so a raw rule with any of
|
|
those set is refused (`raw_conflict`); write them in the text. 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::/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 eth1`) 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 = { 192.168.1.0/24, 192.168.2.0/24 } }
|
|
set trusted_v6 { type ipv6_addr; flags interval; elements = { 2001:db8:1::/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.
|