The boot unit fails closed: if ruleset.nft doesn't load, it loads ruleset-fallback.nft (render.Fallback), saved beside it from the same configuration: nothing new in from external zones, established, the ICMPv6 IPv6 needs and DHCPv6 replies let in, the inside left alone and masqueraded, and the unit marked failed with a critical message. housecarld replaces it when it applies; status, the dashboard and /healthz say while it is in force. no_external_zone now also says the fallback closes everything without an external zone. The boot ruleset has no flowtable: nftables on Debian 13's kernel refuses one naming a device that doesn't exist yet (ppp0 before PPPoE), and with it the whole file. internal/linkwatch listens to rtnetlink link messages; housecarld applies again when a flowtable device appears or comes back as a new interface (by ifindex), under the engine's lock. docs/flowtable.md: what it does, when to use it and when not, how. GET /healthz reports firewall (applied, unconfigured, fallback, missing), changes on probation and the machine's warnings, with 503 unless in force, read at most every five seconds. Renaming a zone, set or service changes what names it in the same change (one history entry, one undo); dynamic sets keep their names. The importer keeps a chain's head in order: once a would-be built-in stays a rule, the ones after it do too, so the maintainer's ICMPv6 accepts stay behind the invalid drop as written. hc settings set --flowtable none empties the list. make test (nft 1.1.3) and make e2e (242 checks, with VLAN sub-interfaces, the fallback and the flowtable) pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
8.4 KiB
Markdown
135 lines
8.4 KiB
Markdown
# Validation
|
|
|
|
Every change is checked as a whole configuration before it is applied.
|
|
An **error** stops the change; a **warning** is reported with it, and
|
|
stays on the dashboard and in `hc status` until fixed. Each problem has a
|
|
stable code, shown by the CLI as `[code]` and in the API as
|
|
`problems[].code`. `housecarld check` runs the checks on the saved
|
|
configuration.
|
|
|
|
After Housecarl's own checks, **nftables** checks the rendered ruleset
|
|
(`nft -c`). What it refuses (mostly raw rules) comes back as a
|
|
`firewall` error with nftables' message, and nothing is changed.
|
|
|
|
The warnings marked *host* compare the configuration with this machine's
|
|
interfaces.
|
|
|
|
## Names
|
|
|
|
Zones, sets, services and port forwards have names (a forward's is
|
|
optional): a letter, then letters, digits, `-` and `_`, up to 48. Names
|
|
are unique per kind, ignoring case.
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `zone_name`, `set_name`, `service_name`, `forward_name` | error | not a valid name, or one that looks like an address |
|
|
| `zone_name_taken`, `set_name_taken`, `service_name_taken`, `forward_name_taken` | error | another object of the kind has that name |
|
|
|
|
## Settings
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `policy` | error | a policy that isn't `accept` or `drop` |
|
|
| `settings_address` | error | an own prefix or antispoof entry that isn't an address or prefix, or is in the wrong list (an IPv6 prefix in the IPv4 list) |
|
|
| `flowtable_device` | error | a flowtable device with a wildcard: name each device |
|
|
| `flowtable_device_missing` | warning, *host* | a flowtable device doesn't exist; the flowtable is left out until it does |
|
|
| `probation_range` | error | the probation isn't 10 to 3600 seconds |
|
|
| `stats_no_external` | warning | traffic per device is on, but no zone is external: it counts what goes to and comes from external zones |
|
|
| `stats_flowtable` | warning | traffic per device with the flowtable on: its fast path skips the forward chain, so only each connection's first packets are counted |
|
|
|
|
## Zones
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `zone_empty` | error | no interfaces |
|
|
| `zone_interface` | error | not an interface name (up to 15 characters; a trailing `*` matches many) |
|
|
| `zone_interface_taken` | error | the interface is in another zone already |
|
|
| `zone_interface_missing` | warning, *host* | the interface doesn't exist here (yet); its rules match once it does |
|
|
| `no_external_zone` | warning | no zone is external: antispoofing, MSS clamping, the DHCPv6 client rule and port forwards without public addresses need one, and the boot fallback lets nothing but loopback through without one ([operations.md](operations.md#if-the-ruleset-fails-to-load-at-boot)) |
|
|
|
|
## Sets and services
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `set_name_reserved` | error | a set name starting with `meter_`, `conn_`, `quota_` or `stats_`: Housecarl's own sets (rate limits, quotas, statistics) use those |
|
|
| `set_entry` | error | an entry that isn't an address, prefix (without host bits) or range |
|
|
| `set_empty` | warning | a set (not dynamic) with no entries: rules using it match nothing |
|
|
| `set_dynamic_entries` | error | configured entries on a dynamic set: its entries come at run time |
|
|
| `set_timeout` | error | a timeout on a set that isn't dynamic, or under a second |
|
|
| `service_empty` | error | a service with no entries |
|
|
| `service_entry` | error | an entry that isn't `PROTO/PORTS` (`tcp`, `udp`, `sctp`; a port or range) or `icmp/TYPE`, `icmpv6/TYPE` with a type nftables knows |
|
|
|
|
## Matches
|
|
|
|
These apply to rules, source NAT and redirects.
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `unknown_zone` | error | a zone that doesn't exist |
|
|
| `match_in_zone` | error | in zones on the output chain (it has no incoming interface) |
|
|
| `match_out_zone` | error | out zones on the input or early chain, or on a redirect |
|
|
| `match_address` | error | an address that can't be read |
|
|
| `unknown_set` | error | a name that is neither an address nor a set |
|
|
| `unknown_service` | error | a service that is neither built in nor yours |
|
|
| `family` | error | a family that isn't `ipv4` or `ipv6` |
|
|
| `match_never` | error | the addresses, services and family can't all match the same packet (IPv4 sources and an `icmpv6` service) |
|
|
|
|
## Rules
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `rule_chain` | error | a chain that isn't `input`, `forward`, `output` or `early` |
|
|
| `rule_action` | error | an action that isn't `accept`, `drop`, `reject` or `log` |
|
|
| `raw_rule` | error | a raw rule over several lines, or with `;` |
|
|
| `raw_conflict` | error | a raw rule with other fields set (zones, addresses, services, limits, log…): it is used as written, so they would be ignored |
|
|
| `early_conntrack` | error | connection states, limits or a connection limit in the early chain, which runs before connection tracking |
|
|
| `early_reject` | error | reject in the early chain: drop instead |
|
|
| `ct_state`, `ct_status` | error | an unknown connection state or status |
|
|
| `rule_day`, `rule_hours` | error | an unknown day, or hours that aren't `HH:MM`, or only one of them |
|
|
| `rule_limit` | error | a rate that isn't a number per second, minute, hour or day |
|
|
| `rule_conn_limit` | error | a connection limit under 1 |
|
|
| `rule_limit_unit` | error | a rate's unit that isn't packets, bytes, kbytes or mbytes |
|
|
| `rule_limit_bytes` | error | a byte rate nftables can't hold: over 4 GB a second, 293 MB a minute, 4.9 MB an hour or 208 KB a day (the message gives it per second) |
|
|
| `rule_limit_key` | error | a limit both per source and per destination |
|
|
| `rule_quota` | error | a quota under 1 byte |
|
|
| `rule_quota_key` | error | a quota both per source and per destination |
|
|
| `rule_quota_reset` | error | a reset that isn't daily, weekly or monthly; a day with another reset than monthly; a day outside 1-31 |
|
|
| `rule_group` | error | a grouping prefix outside 1-32 (IPv4) or 1-128 (IPv6) |
|
|
| `rule_traffic_accept` | error | a rule with a quota or a byte rate that accepts: it is checked before established connections are accepted, so it can only drop, reject or log |
|
|
| `rule_every_packet_accept` | error | a rule checked against every packet (`every_packet`) that accepts: it comes before established connections are accepted, so it can only drop, reject or log |
|
|
| `flowtable_traffic` | warning | a forward rule checked against every packet (a quota, a byte rate, `every_packet`) while the flowtable is on: it only sees each connection's first packets |
|
|
| `reject_with` | error | `tcp-reset` on a rule that matches more than TCP, or with an action other than reject |
|
|
| `log_prefix` | error | a log prefix over 100 characters |
|
|
|
|
## Port forwards
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `forward_protocol` | error | no protocols, or one that isn't `tcp`, `udp` or `sctp` |
|
|
| `forward_ports` | error | the public port isn't a port or range |
|
|
| `forward_to_port` | error | the inside port isn't one port, or is given for a range of public ports |
|
|
| `forward_target` | error | neither `to` nor `to6`, or `to` that isn't one IPv4 address, or `to6` one IPv6 address |
|
|
| `forward_public` | error | no public addresses and no external zone, or an IPv6 public address |
|
|
| `forward_overlap` | error | another forward has the same protocol, port and public address |
|
|
| `forward_hairpin` | warning | hairpin without public addresses: inside clients' connections can't be recognised |
|
|
|
|
## Source NAT and redirects
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `snat_to` | error | `to` isn't one address, or of another family than the rule (give the rule the family) |
|
|
| `snat_everything` | warning | no out zones and no sources: everything leaving the firewall is translated |
|
|
| `redirect_service` | error | no service, or one without TCP, UDP or SCTP ports |
|
|
| `redirect_to_port` | error | the local port isn't one port |
|
|
|
|
## The machine
|
|
|
|
These come from housecarld looking at the machine, not at the
|
|
configuration: the dashboard and `hc status` show them.
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `nftables_service` | warning, *host* | Debian's `nftables.service` is enabled: at boot it loads `/etc/nftables.conf`, and stopping or restarting it runs `nft flush ruleset`, deleting Housecarl's table too. Disable it with `systemctl disable nftables` (without `--now`) |
|
|
| `conntrack_full` | warning, *host* | the connection tracking table is over 80% full; once it is full, the kernel drops new connections. Raise `net.netfilter.nf_conntrack_max`, or find what opens so many ([stats.md](stats.md#warnings)) |
|
|
| `takeover_pending` | warning, *host* | the table you imported from is still in force beside Housecarl's, so a packet must get past both; `hc takeover TABLE` deletes it once Housecarl's rules are right |
|