Files
housecarl/AGENTS.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

114 lines
5.3 KiB
Markdown

# Working on Housecarl
Notes for coding agents (and people) changing this repository. Read this
first, then [docs/design.md](docs/design.md) (how it fits together and
what 0.1 leaves out) and [docs/development.md](docs/development.md)
(layout, tests, releasing).
Housecarl is a firewall manager for Linux: `housecarld` keeps the
machine's nftables ruleset and serves a REST API and a web UI; `hc` is the
CLI; SQLite holds everything. It is the third of a set with Jarl (the BIND
manager, `../jarl`) and Steward (the DHCP server, `../steward`) and copies
their ways: the same auth, socket and web UI helpers, the same release
process, the same look. When in doubt, do it the way Jarl and Steward do.
Go module `housecarl`, `CGO_ENABLED=0`, Linux only for housecarld.
## Ground rules
* **The database is the source of truth.** The ruleset is rendered from
it; never read nftables' state back as configuration (an importer would
be the only exception).
* **Every change goes through one engine**: edit a copy of the
configuration, validate the whole result, render, check with
`nft -c -f`, apply with `nft -f`, and save the objects, the history
entry and the audit record together. Never write to the store from a
handler.
* **Housecarl's own table only.** Write `inet housecarl` and nothing
else; never flush the whole ruleset (Docker, libvirt and others keep
theirs).
* **Don't lock the user out.** Changes made over the network are applied
on probation and rolled back unless confirmed (docs/design.md). An
empty configuration applies nothing. But never force a rule that keeps
SSH or the web UI open: the maintainer doesn't want one.
* **Stay within nftables 1.1.3** (Debian 13). The dev machine's newer nft
accepts things 1.1.3 refuses; the tests with `HOUSECARL_NFT` and the
e2e test use 1.1.3.
* **Every user-visible change updates `CHANGELOG.md`** under
`## [Unreleased]`, and the matching `docs/` page in the same change.
New validation codes get a case in `internal/validate/validate_test.go`
and a row in `docs/validation.md`.
* **Errors** say what is wrong and how to fix it, naming the object.
* **Schema changes** append a migration; never edit a shipped one.
* `make test` runs `go vet` and a `gofmt -l` check. `goimports` isn't
installed, so fix imports by hand.
## The logo and wordmark
* The masters are `artwork/logo.svg` (the carved doorway) and
`artwork/logo-small.svg` (without its carvings, for under about 64 px).
Each is one evenodd path in a shared viewBox; their color comes from
CSS (`--logo`, lighter in the dark theme).
* `make logo` (`scripts/logo.py`) copies them into the web UI
(`assets/logo.js`, and `assets/logo.svg`, the favicon, from the small
one) and runs `scripts/banner.py`, which rebuilds `artwork/banner.svg`.
Never edit those outputs by hand.
* The wordmark's letters are paths in `assets/brand.js`, in the angular
style of Jarl's and Steward's (A, R and L are Jarl's, S and E Steward's).
`banner.py` reads them from there.
## Testing
```sh
make test # vet + gofmt check + unit tests
make e2e # real traffic through a router container (needs Docker)
```
* **Docker** on this machine needs `DOCKER_HOST=unix:///var/run/docker.sock`.
`docker build` can't reach the network here: images are made with
`docker run` + `docker commit` (scripts/e2e.sh makes `housecarl-nft`).
The e2e test prints `RESULT: N passed, 0 failed`; run it after any
change to `render`, `nft`, `engine`, `server`, `importer` or the CLI.
* **Real nft in unit tests:**
`HOUSECARL_NFT="docker run --rm -i --cap-add NET_ADMIN housecarl-nft nft"`
and `HOUSECARL_NFT_IMAGE=housecarl-nft`; without them those tests skip.
Unprivileged namespaces (`unshare -rn`) are blocked here by AppArmor.
* **The systemd sandbox is only tested by a package test**: `make deb`,
installed in a `debian:trixie` container running systemd
(`--privileged --cgroupns=host /sbin/init`).
* **The web UI** has no JS tests. Bundle `assets/app.js` with esbuild to
catch syntax and import errors, and look at it in headless Chrome
against the e2e lab (`KEEP=1`, web UI on `10.88.10.1:8070`).
## Web UI rules
As in Jarl and Steward:
* Build DOM only with `h()` from `ui.js`, never `innerHTML`.
* The CSP is `self` only: no inline styles or scripts. Use a class, or set
`el.style.prop`.
* Colors are the CSS custom properties in `style.css`, with light and dark
values; reuse them.
* Rebuild housecarld after editing, since the files are embedded.
* Function over polish. Leave pick-list checkboxes unticked by default.
## Git and releases
The maintainer's rules:
* **Commit only when asked. Never push.** The maintainer pushes, to Gitea
at gitea.adbraun.lol.
* **Branches** are `dev-X.Y.Z`, never named like a tag.
* **A release** is a commit "Release X": it sets `VERSION`, updates the
README status line, and (for a final release) moves `[Unreleased]` to
`## [X] - YYYY-MM-DD`. The annotated tag is `vX` with message
"Housecarl X".
## Shell hygiene
* **Never kill processes by pattern** (`pkill -f`). The pattern matches
the shell running it. Find the PID with `ps` and kill that.
* **Run long-lived test servers in the background mode of your tool**, not
with `&` in a shell that exits.
* **Never apply a test ruleset to this machine's own nftables.** Test in a
network namespace or a container.