milestone 20: declarative configuration for iac

This commit is contained in:
2026-08-11 23:31:40 +02:00
parent 2f29121e27
commit d76afc147a
74 changed files with 6722 additions and 1949 deletions
+98 -36
View File
@@ -9,8 +9,13 @@ delete the directory.
Follow the steps in order. Each one says what it did.
Every command below was executed on x86_64 Linux with Zig 0.16.0, Node.js
24.14.1, dig 9.20.26 and curl 8.21.0. The only thing substituted during that run
was the tutorial directory.
24.14.1, dig 9.20.26 and curl 8.21.0. Steps 4 to 11, 13 and 14 were re-run end
to end for this revision, and the transcripts are that run's output with the
tutorial directory substituted. Two things were not re-run: the browser page in
step 12 — its endpoints were exercised, the page itself was not opened — and the
two build commands in steps 1 and 2, which had already produced the binary under
test. The ZON block at the end of step 14 was checked with `nxdns check
--config` rather than started.
## What you need
@@ -30,7 +35,7 @@ cd web && npm ci && npm run build && cd ..
```
This produces `web/dist`. Do not skip it. A plain `zig build` embeds
`web/dist-placeholder`, a one-page status stub, and you would reach step 10 and
`web/dist-placeholder`, a one-page status stub, and you would reach step 12 and
find no admin interface there.
## 2. Build nxdns
@@ -75,6 +80,11 @@ itself, so a configuration missing either one is rejected. Whichever command
reads the file says the same thing and stops the same way: `run`, `nxdns check`
and `nxdns import` all print the problem and exit 2.
This tutorial loads the file into the database once and then runs nxdns against
the database, which is what the packaged systemd unit does. There is a second
way to run nxdns, where the file itself stays the configuration; step 14 shows
what changes.
## 4. Check the configuration before starting
```sh
@@ -92,33 +102,44 @@ answers. It exits 2 when it found something that has to be fixed and 0
otherwise. It writes nothing and starts no listener, so you can run it as often
as you like.
## 5. Start the server
In your first terminal:
## 5. Load it into the database
```sh
zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutorial/config.zon
zig-out/bin/nxdns import ~/nxdns-tutorial/config.zon --data-dir ~/nxdns-tutorial/data
```
```
info(migrations): config.db migrated from schema version 0 to 2
info(config_bootstrap): seeded the database from '/home/you/nxdns-tutorial/config.zon'
imported /home/you/nxdns-tutorial/config.zon
```
The data directory did not exist; nxdns created it at mode 0700 along with
`config.db`. From here the database holds the configuration, and you will change
it through the API rather than by editing the file again.
## 6. Start the server
In your first terminal:
```sh
zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data
```
```
info(querylog_schema): created querylog database '/home/you/nxdns-tutorial/data/querylog.db'
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 199 bytes
info(nxdns): authority: database
info(nxdns): nxdns <version> serving on udp [::1]:15353 udp 127.0.0.1:15353 tcp [::1]:15353 tcp 127.0.0.1:15353; 1 upstream(s); blocklist generation 1
info(web_server): web interface listening on 127.0.0.1:8080
```
The data directory did not exist; nxdns created it at mode 0700 along with
`config.db` and `querylog.db`. The line that matters is `seeded the database
from`: the configuration file was read into the database this once. From now on
the database is the truth and the file is ignored on every later start, which
you will see for yourself in step 11. Configuration changes go through the API,
the web interface, or `nxdns import`.
No `--config` here, and that is the point: `authority: database` says the
database is the configuration and no file was opened at all. The file you wrote
in step 3 has done its job.
Leave this terminal running and switch to the second one.
## 6. Resolve a name
## 7. Resolve a name
```sh
dig @127.0.0.1 -p 15353 example.com A +noall +answer
@@ -131,10 +152,10 @@ example.com. 90 IN A 104.20.23.154
nxdns had no answer cached, so it forwarded the query to
`https://cloudflare-dns.com/dns-query` over HTTPS and returned what came back.
You now have a working resolver. It blocks nothing yet: the log line in step 5
You now have a working resolver. It blocks nothing yet: the log line in step 6
said `0 of 0 sources loaded`.
## 7. Add a blocklist source
## 8. Add a blocklist source
```sh
curl -s -X POST http://127.0.0.1:8080/api/blocklists \
@@ -152,7 +173,7 @@ That is fine for a localhost tutorial and wrong for anything else; see
Note the `"id":1` in the response. You need it in the next step.
## 8. Attach the source to the `default` group
## 9. Attach the source to the `default` group
A blocklist source belongs to the installation. Which groups use it is a
separate decision, which is what lets one group get a strict list and another
@@ -190,7 +211,7 @@ curl -s -X PUT http://127.0.0.1:8080/api/groups/1/sources \
{"source_ids":[1]}
```
## 9. Download the list
## 10. Download the list
Adding a source registers it; it does not fetch it. Ask for a refresh:
@@ -199,25 +220,25 @@ curl -s -X POST http://127.0.0.1:8080/api/blocklists/update
```
```json
{"sources":[{"id":1,"state":"ok","loaded":true,"last_attempt":1785683777,"last_success":1785683777,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":99277,"wildcards":0,"skipped_regex":0}]}
{"sources":[{"id":1,"state":"ok","loaded":true,"last_attempt":1786473715,"last_success":1786473715,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":99559,"wildcards":0,"skipped_regex":0}]}
```
The download is about 3 MB and takes a few seconds. Watch the first terminal
until this appears:
```
info(blocklist_manager): blocklist snapshot generation 5: 1 of 1 sources loaded, 3088703 bytes
info(blocklist_manager): blocklist snapshot generation 6: 1 of 1 sources loaded, 3096006 bytes
```
`1 of 1 sources loaded` is the line to wait for. nxdns builds each blocklist
snapshot in full and swaps it in atomically, so queries keep being answered from
the previous snapshot the whole time the new one is being built. After this, the
domain count in the JSON above — 99277 on the day this was run — is live.
domain count in the JSON above — 99559 on the day this was run — is live.
From here on, the list is on disk under `~/nxdns-tutorial/data/blocklists`.
Restarting nxdns does not re-download it.
## 10. Watch a domain get blocked
## 11. Watch a domain get blocked
```sh
dig @127.0.0.1 -p 15353 doubleclick.net A +noall +answer
@@ -249,7 +270,7 @@ yourself can, with a wildcard pattern such as `*.doubleclick.net`. So when you
pick a domain to test, pick one that is literally in the file.
`www.google-analytics.com` is another that is.
## 11. Open the web interface
## 12. Open the web interface
Visit <http://127.0.0.1:8080> in a browser. This is the single-page application
you built in step 1, served out of the binary. The dashboard shows query and
@@ -257,7 +278,7 @@ block counts, and the Blocklists page shows the source you added with its
domain count. (The endpoints behind those two pages were checked while writing
this; the browser page itself was not opened on the verification host.)
## 12. Stop it
## 13. Stop it
In the first terminal, press Ctrl-C.
@@ -267,21 +288,61 @@ info(nxdns): shutting down
nxdns catches SIGINT and SIGTERM, stops serving and exits 0.
Start it again with the same command as in step 5 and read the first log line:
Start it again with the same command as in step 6 and read the first log lines:
```
info(config_bootstrap): configuration file ignored; the database is already configured
info(blocklist_manager): blocklist snapshot generation 1: 1 of 1 sources loaded, 3088703 bytes
info(blocklist_manager): blocklist snapshot generation 1: 1 of 1 sources loaded, 3096006 bytes
info(nxdns): authority: database
```
The configuration file was not even opened, and the blocklist came off disk
rather than the network. The blocklist source, the group it is attached to, and
the query log all survived the restart because they live in
`~/nxdns-tutorial/data`.
The blocklist came off disk rather than the network. The source you added, the
group it is attached to, and the query log all survived the restart because they
live in `~/nxdns-tutorial/data`, which is what `authority: database` means in
practice.
Press Ctrl-C again to stop this second process. Nothing is listening on 15353 or
8080 now, and nothing of nxdns is running.
## 14. See what the other mode does
You have been running in database mode. The alternative is to hand the same file
back as the configuration, which is what `--config` means:
```sh
zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutorial/config.zon
```
```
reconciled '/home/you/nxdns-tutorial/config.zon': sources +0 ~0 -1; group_sources +0 ~0 -1;
info(blocklist_manager): blocklist snapshot generation 1: 0 of 0 sources loaded, 199 bytes
info(nxdns): authority: file (/home/you/nxdns-tutorial/config.zon)
```
**Read that first line.** The blocklist source is gone. That is not a bug — it is
the whole contract. The file you wrote in step 3 never mentioned a blocklist
source, and in file mode the file is the complete statement of what the
configuration is, so anything the database holds that the file does not name is
removed at every start. The reconcile said so in one line before doing it.
The compiled list is still on disk and the query log is untouched; what changed
is the configuration, and it now matches the file exactly.
Neither mode is the "advanced" one. Database mode suits a box someone
administers through the web interface. File mode suits a file kept in git and
deployed by a tool, where the deployed file being what is running matters more
than clicking. To have kept the blocklist here, you would put it in the file:
```zon
.blocklist_sources = .{
.{ .url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", .name = "StevenBlack hosts" },
},
.group_sources = .{
.{ .group = "default", .source_url = "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts" },
},
```
Ctrl-C to stop it.
## What you have now
A resolver that answers real queries, a real blocklist of about 99000 domains
@@ -292,9 +353,10 @@ directory you can delete:
rm -rf ~/nxdns-tutorial
```
You also saw the two things that surprise people most: the configuration file
seeds the database once and is ignored afterwards, and a blocklist source does
nothing until a group uses it.
You also saw the three things that surprise people most: a blocklist source
does nothing until a group uses it, which authority a start runs under is
printed rather than guessed, and in file mode anything the file does not name is
removed at the next start.
## Where to go next
@@ -305,4 +367,4 @@ nothing until a group uses it.
- [reference/configuration.md](../reference/configuration.md) — every field you
did not set.
- [explanation/configuration-model.md](../explanation/configuration-model.md) —
why the file seeds and the database rules.
why there are two authority modes and what each one is for.