Files
abrown38andClaude Opus 5.5 68ba2d06c5 Sets from Jarl: sets holding the addresses of names in Jarl
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>
2026-10-23 13:51:55 +01:00

283 lines
14 KiB
Markdown

# The hc CLI
`hc` talks to housecarld's REST API. `hc help COMMAND` describes each
command and its flags.
## Connecting
* **On the firewall**, with nothing configured, the CLI uses
housecarld's local socket, `/run/housecarl/housecarl.sock`. Root and
the `housecarl` user are administrators there without a token, and
their changes need no confirmation: `sudo hc status`.
* **From elsewhere**, give the server's URL and an API token:
```sh
hc config set server https://fw.example.com:8070
hc config set token # asks for it
```
The settings are in `~/.config/housecarl/config.yaml` (on macOS
`~/Library/Application Support/housecarl/config.yaml`, on Windows
`%AppData%\housecarl\config.yaml`).
| Flag | Environment | |
|---|---|---|
| `-s, --server URL` | `HOUSECARL_SERVER` | the server; `local` for the local socket |
| `--token TOKEN` or `@FILE` | `HOUSECARL_TOKEN` | the API token |
| `--config FILE` | `HOUSECARL_CLI_CONFIG` | the CLI's configuration file |
| | `HOUSECARL_SOCKET` | the local socket's path |
| `--insecure`, `--ca-cert FILE` | | TLS: skip the check, or trust another CA |
| `-n, --dry-run` | | check and show a change without making it, with the ruleset's diff |
| `--no-confirm` | | leave a change on probation instead of confirming it once `hc` reaches Housecarl again |
| `-C, --changeset ID` | `HOUSECARL_CHANGESET` (checked, see [Changesets](#changesets)) | stage configuration changes in this changeset instead of making them |
| `-o, --output FORMAT` | | `table` (default), `json`, `yaml` or `id` |
| `-q, --quiet` | | print only errors (and the IDs of new objects) |
| `--no-names` | | show inside addresses without their names: tables show `192.168.1.20 (laptop)` ([configuration.md](configuration.md#names)) |
Exit codes: 0 ok, 1 error, 3 not authenticated or not allowed, 4 not
found, 5 conflict, 6 invalid or refused by validation or nftables, 7
server error.
## Changes
Every change is checked as a whole, then by nftables, before it is put
in force. A refused change says why, with the problem's code
([validation.md](validation.md)). An applied one says its history entry
and any new warnings. Over the network, it is on probation, and `hc`
confirms it by reaching Housecarl again over a new connection
([operations.md](operations.md#changes-on-probation)):
```
$ hc rule add input --from trusted --service ssh --comment "SSH from home"
created rule input #31 (from trusted · ssh)
applied and confirmed (history 12): this client reached Housecarl again over a new connection
```
Through the socket the change is kept at once:
```
applied and saved (history 12; undo with 'hc history undo 12')
```
`-n` shows what would change without changing it, with the diff of the
ruleset (each `@@` line names the chain the lines below it are in);
nftables checks it too.
## Changesets
To review several changes and apply them together, as one entry in the
history, stage them in a changeset:
```sh
export HOUSECARL_CHANGESET=$(hc changeset create "guest network" -q)
hc zone add guest -i eth1.30
hc rule add forward --in guest --out wan
hc changeset diff # the operations, problems, nftables' check, the ruleset's diff
hc changeset remove 2 # take operation 2 out
hc changeset apply # one change; over the network on probation
unset HOUSECARL_CHANGESET
```
```
$ hc rule add forward --in guest --out wan
changeset 7 (guest network, 1 operation) from HOUSECARL_CHANGESET
staged: create rule forward #52
staged in changeset 7 as operation 2 (2 operations now); review with 'hc changeset diff 7', apply with 'hc changeset apply 7'
```
Each change is checked as it is staged: the changeset with it must stay
valid, and nftables must accept the ruleset, so make a set before the
rule that names it. Applying checks the whole changeset again, on the
configuration as it is then; an operation whose object changed since is
named (`operation 3 (update rule input #12) can't be applied: …`), and
`hc changeset remove` takes it out.
| Command | |
|---|---|
| `changeset create [DESCRIPTION]` | start one; `-q` prints only its ID |
| `changeset list [--status open\|applied\|discarded\|""] [--mine]` | open ones by default |
| `changeset show [ID]` | its operations |
| `changeset diff [ID]` | what applying it would do |
| `changeset apply [ID] [--description D]` | apply it |
| `changeset discard [ID]` | throw it away |
| `changeset remove [ID] OP` | take one operation out |
`show`, `diff`, `apply`, `discard` and `remove` work on the changeset you
name, else the one in `-C`, else the one in `HOUSECARL_CHANGESET`.
A changeset from `HOUSECARL_CHANGESET` is checked before anything uses
it, because an exported ID easily outlives its changeset:
- Staging into it, and `apply`, `discard` and `remove` on it, need it to
be open and yours. Otherwise the command stops and says why (`there is
no changeset 99`, `changeset 42 is applied`, `changeset 57 is sam's`).
Commands that only read, and `hc changeset create`, don't care.
- Each command says on stderr which changeset it uses: `changeset 42
(guest network, 2 operations) from HOUSECARL_CHANGESET` (not with `-q`).
- After `apply` or `discard`, a reminder to `unset HOUSECARL_CHANGESET`.
`-C ID` and an ID argument are used as given (housecarld still refuses a
changeset that isn't open, or another user's unless you are an
administrator). Only configuration changes are staged: dynamic set
entries, `confirm` and `rollback` are made at once; `history undo` is
staged like any change; `import` and `takeover` can't be staged, so they
are refused while a changeset is named. Changesets staged in the web UI show up here too,
and the other way round.
## Commands
Objects are named by ID or, for zones, sets, services and port forwards,
by name.
### status and the ruleset
| Command | |
|---|---|
| `status` | the mode (in shadow mode), the ruleset (in force or not), changes on probation, problems, interfaces, other tables |
| `interfaces` | the machine's interfaces, their addresses and zones |
| `ruleset [--enforcing]` | the nftables ruleset Housecarl builds; in shadow mode, `--enforcing` shows the one loaded once it is off ([shadow.md](shadow.md)) |
| `counters` | the hits of every rule, NAT rule, built-in and policy, by mark (`r12`, `f3`, `b-established`, `p-input`) |
| `quotas` | what each rule's quota used (each address's, for per-address quotas), and when it resets |
| `log [-f] [--search TEXT] [--limit N]` | the packets logged by rules and policies ([operations.md](operations.md#the-log)) |
| `connections [--search TEXT] [--limit N]`, `connections close --search TEXT [-y]` | tracked connections; closing makes the rules check them again ([operations.md](operations.md#connections)); not in shadow mode |
| `stats [--since 24h]` | traffic per zone, drops, the busiest rules, blocked sources, devices; `stats status`, `stats traffic`, `stats top devices\|blocked\|tried\|quota ID`, `stats rules [--unused]`, `stats connections` ([stats.md](stats.md#the-cli)) |
| `matrix [FROM TO]` | what the rules do to new connections between zones (allow, block or partly), and what is let through; with FROM and TO, that pair's rules in order, with their hits ([rules.md](rules.md#the-zone-matrix)) |
| `trace [--address A] [--port N] [--protocol P] [--seconds S]` | follow packets through every table's rules ([operations.md](operations.md#tracing)) |
### zone, set, service
| Command | |
|---|---|
| `zone list`, `zone show ZONE` | |
| `zone add NAME -i IFACE... [--external] [--comment C]` | |
| `zone update ZONE [-i IFACE]... [--add IFACE] [--remove IFACE] [--external[=false]] [--name N] [--comment C]` | `-i` replaces the interfaces; `--name` renames it, and the rules naming it follow |
| `zone delete ZONE` | refused while rules use it |
| `set list`, `set show SET` | |
| `set add NAME [ENTRY...] [--dynamic] [--timeout 24h] [--comment C]` | |
| `set add NAME --jarl NAME[@VIEW]... --within ZONE\|NETWORK...` | a set from Jarl: the names' addresses, only those within the zones and networks ([jarl.md](jarl.md#sets-from-jarl)) |
| `set update SET [--entry E]... [--add E] [--remove E] [--timeout] [--jarl N]... [--within W]... [--name] [--comment]` | `--entry` replaces the entries, `--jarl` a Jarl set's names, `--within` where its addresses may be |
| `set entries SET`, `set entries add SET ADDRESS [--timeout 1h]`, `set entries remove SET ADDRESS` | a dynamic set's entries |
| `set jarl SET [--refresh]` | a set from Jarl: each name's addresses in Jarl (CNAMEs followed), used or why not, and when Jarl answered; `--refresh` reads Jarl first |
| `set delete SET` | |
| `service list`, `service show SERVICE` | with the built-in ones |
| `service add NAME ENTRY... [--comment C]` | entries `tcp/22`, `udp/1000-2000`, `icmp/echo-request`, `icmpv6/packet-too-big` |
| `service update SERVICE`, `service delete SERVICE` | |
See [rules.md](rules.md).
### rule
| Command | |
|---|---|
| `rule list [CHAIN]` | in order, with their hits |
| `rule show ID` | |
| `rule add CHAIN [MATCH] [-a ACTION] [--first\|--before ID\|--after ID]` | at the end of its chain unless placed |
| `rule update ID [flags]` | only the flags given |
| `rule move ID --before ID\|--after ID\|--first\|--last\|--chain CHAIN` | |
| `rule enable\|disable ID` | |
| `rule reset-quota ID [--address ADDR] [--give 5gbytes]` | start a rule's quota again now, or one address's; `--give` takes an amount off what was used instead, until the next reset |
| `rule delete ID` | |
The match flags (`rule add`, `snat add`, `redirect add`):
| | |
|---|---|
| `--in ZONE`, `--out ZONE` (`--not-in`, `--not-out`) | zones |
| `--from`, `--to` (`--not-from`, `--not-to`) | sets, addresses, prefixes, ranges |
| `--service` | services or entries |
| `--family ipv4\|ipv6` | |
And for rules: `--state`, `--status`, `--days`, `--hours`, `--limit
10/minute|1mbytes/second[,burst=N][,over][,per-source|,per-destination]`,
`--quota 50gbytes[,over][,per-source|,per-destination][,daily|,weekly|,monthly=DAY]`,
`--group-v4 N`, `--group-v6 N`, `--conn-limit N`, `--every-packet`, `-a
accept|drop|reject|log`, `--reject-with tcp-reset`, `--log`,
`--log-prefix`, `--raw RULE`, `--comment`, `--disabled`
([rules.md](rules.md#rules)).
### forward, snat, redirect
| Command | |
|---|---|
| `forward list`, `forward show FORWARD` | port forwards, with their hits |
| `forward add PORTS --to HOST[:PORT] [--proto tcp,udp] [--public ADDR]... [--from SRC]... [--hairpin] [--hairpin-from SRC]... [--to6 HOST] [--name N] [--comment C]` | |
| `forward update FORWARD [flags]`, `forward delete FORWARD` | |
| `snat list`, `snat add [MATCH] [--snat-to ADDR]`, `snat update ID`, `snat delete ID` | source NAT, in order; masquerade without `--snat-to` |
| `redirect list`, `redirect add [MATCH] [--to-port PORT]`, `redirect update ID`, `redirect delete ID` | |
See [nat.md](nat.md).
### banlist, ban
| Command | |
|---|---|
| `banlist list`, `banlist show BANLIST` | ban lists, with their bans' count |
| `banlist add NAME --scope firewall\|hosts\|everything [--host ADDR\|SET]... [--never ADDR\|SET]... [--time 1h] [--max-time 7d\|none] [--max-entries N] [--group-v6 64] [--disabled] [--comment C]` | |
| `banlist update BANLIST [flags]`, `banlist delete BANLIST` | `--host` and `--never` replace the lists; no renaming |
| `bans [BANLIST]` | the active bans |
| `ban BANLIST ADDRESS [--tcp PORT\|SERVICE]... [--udp …]... [--time 1h\|none] [--reason R]` | ban every port, or some; again renews |
| `unban BANLIST ADDRESS\|ID` | lift one ban, or all of an address's |
See [bans.md](bans.md).
### settings
| Command | |
|---|---|
| `settings show` | |
| `settings set [--input-policy\|--forward-policy\|--output-policy accept\|drop] [--log-drops on\|off] [--builtin NAME=on\|off]... [--own-prefix P]... [--antispoof-v4 P]... [--antispoof-v6 P]... [--flowtable DEV]... [--probation 2m] [--auto-confirm on\|off] [--stats-per-device on\|off]` | lists given replace the old ones; `none` empties one (`--flowtable none`: off) |
### probation, history, import
| Command | |
|---|---|
| `confirm` | keep the changes on probation; the ruleset is saved for the boot |
| `rollback` | undo the changes on probation now |
| `history list [--limit N]` | changes, newest first |
| `history show ID` | each object before and after (a changed setting by its own fields: `builtins.drop_invalid: false -> true`) |
| `history diff ID` | what the change did to the ruleset |
| `history undo ID` | apply the change backwards (refused if something it touched has changed since) |
| `import [FILE\|-] [--text FILE] [--defines FILE] [--table T] [--zone IFACE=NAME]... [--replace]` | [import.md](import.md) |
| `takeover TABLE... [-y]` | delete tables Housecarl's ruleset replaces (administrators); not in shadow mode |
### cert
Administrators only ([certificates.md](certificates.md)).
| Command | |
|---|---|
| `cert` | the certificate: name, CA, validity, renewal, the last check |
| `cert check` | check that the challenge can be published through Jarl and seen, without the CA (exit code 1 if it can't) |
| `cert renew [-w]` | get a new certificate now; `-w` waits and shows the result |
### users, tokens, audit
| Command | |
|---|---|
| `whoami` | who you are to the server, and what you may do |
| `user list\|add\|update\|delete` | users (administrators); `user add NAME --role operator` |
| `token list [--all]`, `token create NAME [--role R] [--grant G]... [--expires 90d] [--user U] [--save]`, `token revoke ID` | API tokens |
| `token rotate ID\|--current [--grace 1h] [--expires 90d\|never] [--save]` | a new secret for a token, the old one working for the grace period |
| `password` | change your password |
| `audit [--user U] [--action PREFIX] [--search TEXT]` | the audit log |
| `config show`, `config set server\|token\|insecure\|ca_cert [VALUE]` | this CLI's settings |
[permissions.md](permissions.md) explains roles and grants.
## housecarld
The daemon's own commands work on the database directly, as root or the
`housecarl` user (`sudo -u housecarl housecarld ...`):
| Command | |
|---|---|
| `housecarld serve` | run (the systemd unit does) |
| `housecarld init [-u NAME] [--token]` | create the database and the first administrator (asks for the name on a terminal) |
| `housecarld user add NAME [--admin]`, `user password NAME`, `user token NAME TOKEN-NAME` | when the API can't be used |
| `housecarld check` | check the configuration against this machine's interfaces |
| `housecarld backup [FILE]`, `housecarld restore FILE` | [operations.md](operations.md) |
| `housecarld config [--example]` | the settings in use, or a commented example |
| `housecarld version` | |
All take `-c FILE` for another configuration file.