Add changesets, bandwidth limits and quotas; generic examples

Changesets, as in Jarl: configuration changes are operations that can be
staged in a changeset, checked as they are added (validation and nft -c),
reviewed with the diff of the ruleset, and applied together as one
history entry, then on probation like any change made over the network.
The web UI collects edits for review by default (pending rows, a bar with
Review and Apply now, a Changes page, a switch for live edits); hc stages
with -C ID or HOUSECARL_CHANGESET (checked like JARL_CHANGESET) and has
hc changeset create/list/show/diff/apply/discard/remove. Undo, import and
takeover refuse a changeset. --dry-run shows the ruleset's diff.

Bandwidth and quotas: rate limits in bytes, per source or destination;
quotas per rule or per address that reset daily, weekly, monthly on a
day, or by hand, with their usage carried across reloads, saved for
reboots, shown by hc quotas and on the rules page, and reset with hc rule
reset-quota. Rules that count traffic are checked before established
connections are accepted (so they see every packet) and can't accept.
Per-address limits group IPv6 by /64 by default (--group-v4/-v6).
Per-source rate meters use update. The importer turns meters, byte rates
and quotas into these fields.

The examples in the web UI, hc's help and the docs use documentation
addresses and generic names instead of the maintainer's network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-19 11:18:54 +01:00
1 parent ebec2cfcca
commit 2880f7ddc9
63 files changed
+4018 -385

No files matched your search

+65 -12
View File
@@ -3,12 +3,12 @@
## 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
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 enp2s0 --interface 'enp2s0.*'
hc zone add lan --interface eth1 --interface 'eth1.*'
hc zone add wg --interface wg0
```
@@ -27,14 +27,14 @@ A set is a named list of IPv4 and IPv6 addresses, prefixes and ranges,
mixed:
```sh
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
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 10.0.10.3`); a set is better when several rules share
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,
@@ -43,7 +43,7 @@ and nftables removes them when it runs out, even with housecarld stopped:
```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:7f3d:30::17 --timeout 2h
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
```
@@ -60,7 +60,7 @@ A service names ports of a protocol (`tcp`, `udp`, `sctp`) and ICMP
types:
```sh
hc service add jellyfin tcp/8096 tcp/8920
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
```
@@ -108,7 +108,8 @@ A rule matches on what it gives, and all of it:
| `--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) |
| `--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
@@ -131,6 +132,58 @@ 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
@@ -162,12 +215,12 @@ UI), and `hc ruleset` shows them:
| `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
**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 enp2s0`) lets established
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.
@@ -179,8 +232,8 @@ 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 } }
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"