POST /rules/{ref}/reset-quota takes an optional address (one entry of a
per-address quota starts again) and give (bytes taken off what was used,
one address's or everyone's, until the next reset). The firewall keeps
the adjustment for its next apply, which writes the new usage into the
ruleset; housecarld applies at once and saves the usage. hc rule
reset-quota --address/--give; Reset and Give back in the web UI, per
address in the usage dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
389 lines
18 KiB
Markdown
389 lines
18 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.
|
|
|
|
**VLANs.** A VLAN sub-interface is an interface like any other, by its
|
|
name: `enp2s0.20` (or whatever your network configuration calls it,
|
|
`vlan20`, `br0.20`). Tagged traffic arrives on the sub-interface, and
|
|
untagged traffic on the parent, so `enp2s0` alone doesn't cover
|
|
`enp2s0.20`. Give each VLAN its own zone to keep them apart:
|
|
|
|
```sh
|
|
hc zone add lan --interface enp2s0 # untagged
|
|
hc zone add iot --interface enp2s0.20
|
|
hc zone add guest --interface enp2s0.30
|
|
hc rule add forward --in iot --out wan
|
|
```
|
|
|
|
or one zone for the parent and all of its VLANs, when they should be
|
|
treated alike: `--interface enp2s0 --interface 'enp2s0.*'`. Interface
|
|
names are at most 15 characters (the kernel's limit). The flowtable
|
|
takes VLAN sub-interfaces too, by exact name
|
|
([flowtable.md](flowtable.md)).
|
|
|
|
**Renaming** a zone (`hc zone update dhcp --name lan`, or Edit on the
|
|
Zones page) changes every rule and NAT rule that names it, in the same
|
|
change: the history shows them together, and undoing it puts them all
|
|
back. Sets and services rename the same way, except dynamic sets: their
|
|
entries, and the API tokens and programs that fill them, go by their
|
|
name.
|
|
|
|
## 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.
|
|
`--every-packet` ("every packet" in the web UI) checks a drop or reject
|
|
against every packet, before established connections are accepted, so it
|
|
also stops connections already open, not just new ones; it can't accept
|
|
([operations.md](operations.md#connections)).
|
|
|
|
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
|
|
hc rule reset-quota 31 --address 192.168.1.23 # one device only
|
|
hc rule reset-quota 31 --give 5gbytes --address 192.168.1.23
|
|
```
|
|
|
|
`--give` is a top-up: it takes that much off what was used (never below
|
|
zero), so the device gets 5 GB more until the quota's next reset, and the
|
|
rule stays as it is. The web UI has **Reset** and **Give back…** on the
|
|
rule, and for each address in its usage dialog. Like a reset, these are
|
|
done at once (audited, not staged or on probation); Housecarl puts the
|
|
ruleset in force again with the new figure.
|
|
|
|
In the web UI each quota's use is on its rule (with **Reset**), all of
|
|
them together on the dashboard, and their history under **Traffic →
|
|
Statistics**.
|
|
|
|
| | |
|
|
|---|---|
|
|
| `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;
|
|
[flowtable.md](flowtable.md)).
|
|
|
|
**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.
|
|
|
|
In shadow mode ([shadow.md](shadow.md)) a raw rule's final `drop` or
|
|
`reject` becomes `accept`; a raw rule that does anything else to packets
|
|
(NAT, `meta mark set`, `jump`, `notrack`, a verdict map…) is left out,
|
|
with a warning (`shadow_raw_left_out`).
|
|
|
|
## 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 when the CPU can't keep up with the line. It needs exact
|
|
device names, and costs quotas, byte rates and traffic per device in the
|
|
forward chain their accuracy: see [flowtable.md](flowtable.md) for when
|
|
to use it, and when not.
|
|
|
|
## The zone matrix
|
|
|
|
The Matrix page (`hc matrix`) sums up what the rules do to **new
|
|
connections** between zones: a row per zone the traffic comes from, a
|
|
column per zone it goes to, and the firewall itself (to it: the input
|
|
chain; from it: output; zone to zone: forward).
|
|
|
|
```
|
|
$ hc matrix
|
|
FROM \ TO guest lan wan firewall
|
|
guest block * block partly +1 partly +4
|
|
lan block block * allow partly +4
|
|
wan partly +1 -1 partly +1 -1 block -1 * partly +3 -1
|
|
firewall allow allow allow -
|
|
|
|
Let through for part of the traffic:
|
|
guest → wan http, https
|
|
guest → firewall ICMPv6; ping; ssh from admins; dns
|
|
lan → firewall ICMPv6; ping; ssh from admins; dns
|
|
wan → guest 1 port forward
|
|
wan → lan 1 port forward
|
|
wan → firewall ICMPv6; DHCPv6; ssh from admins
|
|
|
|
allow, block: every new connection; partly: some are let through, some not.
|
|
+N: rules that let part of the traffic through; -N: rules that block part of it. 'hc matrix FROM TO' lists them.
|
|
*: a zone to itself: only traffic routed between its interfaces passes the firewall.
|
|
```
|
|
|
|
Each cell walks that pair's rules in order, as nftables would, with the
|
|
built-ins that act on new connections (ping from internal zones,
|
|
ICMPv6, the DHCPv6 client, antispoofing, port forwards):
|
|
|
|
* a rule that applies to the whole pair decides it;
|
|
* a rule that applies to part of its traffic (some addresses, services,
|
|
times, rates, a quota) is an **exception**, and the walk goes on;
|
|
* what's left falls to the deciding rule or the chain's policy.
|
|
|
|
A cell reads **allow** or **block** when every new connection gets the
|
|
same, and **partly** when exceptions let some through while the rest is
|
|
blocked (or block some while the rest is allowed). **+N** counts the
|
|
exceptions that let traffic through, **-N** those that block it. Under
|
|
the grid (in the web UI, in each cell) is what the exceptions let
|
|
through: their services, with the addresses they are limited to.
|
|
|
|
A zone to itself (`*`, faded in the web UI) is mostly traffic that never
|
|
reaches the firewall: within one interface's network it goes straight
|
|
from host to host. Only what is routed between the zone's interfaces (or
|
|
back out of the one it came in on) passes.
|
|
|
|
`hc matrix guest wan` (or clicking the cell) lists the exceptions in
|
|
order, with each rule's hits:
|
|
|
|
```
|
|
guest → wan (the forward chain), new connections: partly allowed
|
|
VERDICT WHAT BY HITS
|
|
allowed http, https rule forward #14 1520
|
|
blocked the rest forward policy 37
|
|
```
|
|
|
|
The hits are the rule's packets since housecarld started, in all of its
|
|
traffic: a rule without zones counts for every pair it covers. They are
|
|
how you tell an exception that is used from one that isn't.
|
|
|
|
A raw rule is read when it is a match followed by `accept`, `drop` or
|
|
`reject` (with `counter` and `log` if you like): it counts as an exception
|
|
for every pair of its chain, shown as `raw: …`. One that matches
|
|
interfaces (the matrix can't tell which zones), jumps, translates, sets
|
|
something or uses a verdict map shows as **unknown**: check it yourself.
|
|
Raw rules have hits only if they say `counter`.
|
|
|
|
A rule whose addresses all lie in networks directly connected to other
|
|
zones is left out of a pair: traffic between those zones can't have them
|
|
(`from wg_net` doesn't concern traffic coming in on the LAN). Routed
|
|
networks, mixed lists and dynamic sets might, so they stay. Replies and
|
|
established connections aren't shown: the built-in lets them through. In
|
|
review mode, the matrix shows your pending changes.
|
|
|
|
## 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.
|