A set can take its addresses from Jarl: every A and AAAA record of its
names (NAME@VIEW), CNAMEs followed within Jarl's zones in the same view.
housecarld reads them every jarl.refresh and after each change, keeps
only the addresses within the zones and networks the set allows, and
keeps what it last read (saved in the database and the boot ruleset)
while Jarl can't be reached. A change of the addresses applies the
ruleset again without a history entry and is audited (set.jarl).
hc set add --jarl --within, hc set jarl; the kind "from Jarl" and
Addresses… on the Sets page; GET /sets/{name}/jarl and /jarl/refresh;
validation codes set_jarl_*, status warnings jarl_*; docs/jarl.md.
e2e phase with scripts/fakejarl.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
12 KiB
Markdown
164 lines
12 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_`, `stats_` or `ban_`: Housecarl's own sets (rate limits, quotas, statistics, ban lists) 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, not from Jarl) 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 |
|
|
| `set_jarl_entries` | error | configured entries on a set from Jarl: its addresses are its names' ([jarl.md](jarl.md#sets-from-jarl)) |
|
|
| `set_jarl_dynamic` | error | a set both dynamic and from Jarl |
|
|
| `set_jarl_names` | error | a set from Jarl without names, with one that isn't a DNS name (a wildcard, say) or a bad view, or with a name twice |
|
|
| `set_jarl_within` | error | a set from Jarl without `within`, or with an entry there that is neither a zone nor an address, network or range |
|
|
| `set_jarl_anywhere` | warning | a set from Jarl `within 0.0.0.0/0` or `::/0`: any address may come in, so whoever changes the names in Jarl decides what it holds |
|
|
| `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 |
|
|
| `quota_restarts` | warning | a change makes a rule count its quota so that what it used can't carry over (a total to per address, per source to per destination, finer groups): it starts again. Only on the change that does it |
|
|
| `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 |
|
|
| `shadow_unprotected` | warning, *host* | shadow mode, and no other table has a filter chain on the input hook: Housecarl's ruleset blocks nothing, and nothing else protects the machine. Load your firewall, or leave shadow mode ([shadow.md](shadow.md)) |
|
|
| `jarl_name` | warning, *host* | a set from Jarl has a name that gives no addresses, and why: no A, AAAA or CNAME record, a wildcard only, a CNAME out of Jarl's zones or in a loop, below a delegation, a zone Jarl doesn't hold the records of, or in more than one view without one named ([jarl.md](jarl.md#what-a-name-gives)) |
|
|
| `jarl_outside` | warning, *host* | an address a name has in Jarl is left out of its set: it isn't within the set's zones and networks ([jarl.md](jarl.md#within)) |
|
|
| `jarl_unreachable` | warning, *host* | reading Jarl failed (unreachable, the token refused…), for every name or one; the sets keep the addresses they had ([jarl.md](jarl.md#when-jarl-cant-be-reached)) |
|
|
| `jarl_not_configured` | warning, *host* | sets from Jarl, but no `jarl.server` in `housecarl.yaml`: they keep the addresses they last had |
|
|
| `shadow_raw_left_out` | warning, *host* | shadow mode leaves this raw rule out: it does more to packets than accept, drop or reject (NAT, setting a mark or field, a jump, a verdict map…). Its hits aren't counted, and the rules after it see what it would have taken ([shadow.md](shadow.md#what-changes-in-the-ruleset)) |
|
|
|
|
In shadow mode `nftables_service` and `takeover_pending` aren't raised:
|
|
another firewall beside Housecarl is the point there.
|
|
|
|
## Ban lists
|
|
|
|
| Code | | When |
|
|
|---|---|---|
|
|
| `banlist_name` | error | not a valid name (as for zones and sets) |
|
|
| `banlist_name_taken` | error | two ban lists with one name |
|
|
| `banlist_scope` | error | a scope other than `firewall`, `hosts` or `everything` |
|
|
| `banlist_hosts` | error | scope `hosts` without hosts, or hosts with another scope |
|
|
| `banlist_address` | error | a host or never entry that isn't an address, prefix or range |
|
|
| `unknown_set` | error | a host or never entry naming a set that doesn't exist |
|
|
| `banlist_time` | error | negative times (but `max_time` -1), or a default longer than the longest |
|
|
| `banlist_entries` | error | `max_entries` below 0 or above 1000000 |
|
|
| `banlist_group` | error | `group_v6` outside 16 to 128 |
|