Files
housecarl/docs/validation.md
T
abrown38andClaude Opus 5.5 34292eeebb Build Housecarl 0.1.0: the nftables firewall manager
Zones, address sets (IPv4 and IPv6 mixed, dynamic sets with timeouts) and
services, ordered rules in the early, input, forward and output chains,
port forwards with hairpin NAT and IPv6 pinholes, source NAT, redirects,
and switchable built-ins (loopback, conntrack, ICMPv6, antispoofing, MSS
clamping, DHCPv6 client, flowtable). Every change is validated, checked
with nft -c and applied in one transaction to Housecarl's own table,
inet housecarl; changes made over the network roll back unless
confirmed, and the confirmed ruleset is loaded at boot by
housecarl-ruleset.service. An empty configuration applies nothing.

Also: hit counters and the drop log (nflog), 'hc import' of an existing
nftables ruleset and 'hc takeover', the hc CLI, the REST API and the web
UI, users, scoped tokens (sets:write:NAME), the audit log, the web UI's
certificate through Jarl, Debian packages, docs, unit tests with real
nft 1.1.3, and an e2e test with real traffic (71 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 07:57:14 +01:00

109 lines
5.5 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 |
## 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 |
## Sets and services
| Code | | When |
|---|---|---|
| `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_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 `;` |
| `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 |
| `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 |