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>
8.4 KiB
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) |
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) |
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 |