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>
114 lines
5.3 KiB
Markdown
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.
|