# 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*`. ```sh 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: ```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 ``` 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: ```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 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 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. ```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`, 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](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 ``` ### 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 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.