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>
5.3 KiB
5.3 KiB
Working on Housecarl
Notes for coding agents (and people) changing this repository. Read this first, then docs/design.md (how it fits together and what 0.1 leaves out) and 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 withnft -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 housecarland 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_NFTand the e2e test use 1.1.3. - Every user-visible change updates
CHANGELOG.mdunder## [Unreleased], and the matchingdocs/page in the same change. New validation codes get a case ininternal/validate/validate_test.goand a row indocs/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 testrunsgo vetand agofmt -lcheck.goimportsisn't installed, so fix imports by hand.
The logo and wordmark
- The masters are
artwork/logo.svg(the carved doorway) andartwork/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, andassets/logo.svg, the favicon, from the small one) and runsscripts/banner.py, which rebuildsartwork/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.pyreads them from there.
Testing
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 buildcan't reach the network here: images are made withdocker run+docker commit(scripts/e2e.sh makeshousecarl-nft). The e2e test printsRESULT: N passed, 0 failed; run it after any change torender,nft,engine,server,importeror the CLI. - Real nft in unit tests:
HOUSECARL_NFT="docker run --rm -i --cap-add NET_ADMIN housecarl-nft nft"andHOUSECARL_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 adebian:trixiecontainer running systemd (--privileged --cgroupns=host /sbin/init). - The web UI has no JS tests. Bundle
assets/app.jswith esbuild to catch syntax and import errors, and look at it in headless Chrome against the e2e lab (KEEP=1, web UI on10.88.10.1:8070).
Web UI rules
As in Jarl and Steward:
- Build DOM only with
h()fromui.js, neverinnerHTML. - The CSP is
selfonly: no inline styles or scripts. Use a class, or setel.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 isvXwith message "Housecarl X".
Shell hygiene
- Never kill processes by pattern (
pkill -f). The pattern matches the shell running it. Find the PID withpsand 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.