391 lines
15 KiB
Markdown
391 lines
15 KiB
Markdown
# Your first nxdns
|
|
|
|
This page takes you from a checkout of the repository to a running nxdns that
|
|
answers DNS queries, blocks domains from a real blocklist, and shows you the web
|
|
interface. It runs on unprivileged ports in a scratch directory, so nothing on
|
|
your machine changes and nothing needs root. At the end you stop the server and
|
|
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. Steps 2, 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
|
|
`npm` build in step 1, whose `web/dist` was already on disk and is the one the
|
|
binary under test embeds. The ZON block at the end of step 14 was checked with
|
|
`nxdns check --config` rather than started.
|
|
|
|
## What you need
|
|
|
|
- Zig 0.16.0 and Node.js 24, both on your `PATH`.
|
|
- `dig` (from bind-tools or dnsutils) and `curl`.
|
|
- Working internet access: nxdns queries an upstream resolver over HTTPS and
|
|
downloads a 3 MB blocklist.
|
|
- Two terminals. The server runs in the first one; you type into the second.
|
|
|
|
Build commands run from the repository root. The server and the queries use
|
|
`~/nxdns-tutorial` for everything they write.
|
|
|
|
## 1. Build the web interface
|
|
|
|
```sh
|
|
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 12 and
|
|
find no admin interface there.
|
|
|
|
## 2. Build nxdns
|
|
|
|
```sh
|
|
zig build -Dweb-dist=web/dist
|
|
```
|
|
|
|
The binary is `zig-out/bin/nxdns`, with `web/dist` embedded in it. SQLite and
|
|
mbedTLS are vendored and built by this command, so the first build takes a
|
|
while; later builds are cached.
|
|
|
|
## 3. Write a configuration file
|
|
|
|
```sh
|
|
mkdir -p ~/nxdns-tutorial
|
|
cat > ~/nxdns-tutorial/config.zon <<'EOF'
|
|
.{
|
|
.dns = .{ .bind_ipv4 = "127.0.0.1", .bind_ipv6 = "::1", .port = 15353 },
|
|
.web = .{ .bind = "127.0.0.1", .port = 8080 },
|
|
.groups = .{
|
|
.{ .name = "default" },
|
|
},
|
|
.upstreams = .{
|
|
.{ .url = "https://cloudflare-dns.com/dns-query" },
|
|
},
|
|
}
|
|
EOF
|
|
```
|
|
|
|
Four things are set, and the rest of nxdns keeps its defaults.
|
|
|
|
DNS is on port 15353 instead of 53, and both listeners are bound to localhost.
|
|
Port 53 is privileged: binding it needs root or `CAP_NET_BIND_SERVICE`, which is
|
|
an install decision, not a first-run decision. A real install is covered in
|
|
[how-to/install-with-systemd.md](../how-to/install-with-systemd.md).
|
|
|
|
The `default` group and one enabled upstream are the two things nxdns will not
|
|
start without. Every client that nxdns has never seen is assigned to `default`,
|
|
and with no usable upstream there is nowhere to send a query it cannot answer
|
|
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
|
|
zig-out/bin/nxdns check --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutorial/config.zon
|
|
```
|
|
|
|
```
|
|
checking configuration file /home/you/nxdns-tutorial/config.zon
|
|
OK upstreams[0] https://cloudflare-dns.com
|
|
OK: no problems found
|
|
```
|
|
|
|
`check` parses the file, validates it, and contacts each upstream to confirm it
|
|
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. Load it into the database
|
|
|
|
```sh
|
|
zig-out/bin/nxdns import ~/nxdns-tutorial/config.zon --data-dir ~/nxdns-tutorial/data
|
|
```
|
|
|
|
```
|
|
info(migrations): config.db migrated from schema version 0 to 1
|
|
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, 231 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
|
|
```
|
|
|
|
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.
|
|
|
|
## 7. Resolve a name
|
|
|
|
```sh
|
|
dig @127.0.0.1 -p 15353 example.com A +noall +answer
|
|
```
|
|
|
|
```
|
|
example.com. 229 IN A 172.66.147.243
|
|
example.com. 229 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 6
|
|
said `0 of 0 sources loaded`.
|
|
|
|
## 8. Add a blocklist source
|
|
|
|
```sh
|
|
curl -s -X POST http://127.0.0.1:8080/api/blocklists \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","name":"StevenBlack hosts"}'
|
|
```
|
|
|
|
```json
|
|
{"id":1,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","name":"StevenBlack hosts","enabled":true,"is_suggested":false}
|
|
```
|
|
|
|
The API is unauthenticated here because the configuration set no `web.password`.
|
|
That is fine for a localhost tutorial and wrong for anything else; see
|
|
[how-to/set-up-admin-authentication.md](../how-to/set-up-admin-authentication.md).
|
|
|
|
Note the `"id":1` in the response. You need it in the next step.
|
|
|
|
## 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
|
|
get none. A source that is attached to no group is downloaded and then filters
|
|
nothing.
|
|
|
|
You are in that state right now, between the previous step and this one, and it
|
|
is legal rather than wrong — creating a source and attaching it afterwards is
|
|
the normal order, which is why the API accepted it without complaint. On a
|
|
stopped server `nxdns check` names such a source in a `WARN` line and still
|
|
exits 0, so a list that silently blocks nothing is something you can find out
|
|
about later. Attaching it now is what makes it take effect.
|
|
|
|
Ask which groups exist:
|
|
|
|
```sh
|
|
curl -s http://127.0.0.1:8080/api/groups
|
|
```
|
|
|
|
```json
|
|
{"groups":[{"id":1,"name":"default","safe_search":false}]}
|
|
```
|
|
|
|
The `default` group is id 1. Give it the source you just created — the request
|
|
body is the complete set of sources for that group, so sending `[1]` replaces
|
|
whatever was there:
|
|
|
|
```sh
|
|
curl -s -X PUT http://127.0.0.1:8080/api/groups/1/sources \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"source_ids":[1]}'
|
|
```
|
|
|
|
```json
|
|
{"source_ids":[1]}
|
|
```
|
|
|
|
## 10. Download the list
|
|
|
|
Adding a source registers it; it does not fetch it. Ask for a refresh:
|
|
|
|
```sh
|
|
curl -s -X POST http://127.0.0.1:8080/api/blocklists/update
|
|
```
|
|
|
|
```json
|
|
{"sources":[{"id":1,"state":"ok","loaded":true,"last_attempt":1786629237,"last_success":1786629238,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":97648,"wildcards":0,"exceptions":0,"skipped_regex":0,"skipped_unsupported":0}]}
|
|
```
|
|
|
|
The four zeros describe this particular download, not the hosts format.
|
|
`wildcards` counts entries covering a name and its subdomains, `exceptions`
|
|
counts the `@@` lines an Adblock Plus list uses to lift a name another list
|
|
blocks, and `skipped_regex` counts the regex lines nxdns declines to take from a
|
|
downloaded list. `skipped_unsupported` counts lines nxdns cannot translate into a
|
|
DNS decision, and a large value next to a small `domains` means the list targets
|
|
browsers rather than DNS. Only `exceptions` is Adblock-Plus-only: a hosts list
|
|
can carry regex lines, `*.`-prefixed wildcards, and bare sink addresses that
|
|
count as unsupported. This one carries none of them.
|
|
|
|
The download is about 3 MB and takes a few seconds. Watch the first terminal
|
|
until this appears:
|
|
|
|
```
|
|
info(blocklist_manager): blocklist snapshot generation 6: 1 of 1 sources loaded, 2523973 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 — 97648 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.
|
|
|
|
## 11. Watch a domain get blocked
|
|
|
|
```sh
|
|
dig @127.0.0.1 -p 15353 doubleclick.net A +noall +answer
|
|
```
|
|
|
|
```
|
|
doubleclick.net. 5 IN A 0.0.0.0
|
|
```
|
|
|
|
`0.0.0.0` with a TTL of 5 is a block, not an answer. The default block response
|
|
is the zero address and the default block TTL is 5 seconds, so a client that
|
|
caches the answer forgets it quickly after you unblock something.
|
|
|
|
A name that is not on the list still resolves normally:
|
|
|
|
```sh
|
|
dig @127.0.0.1 -p 15353 wikipedia.org A +noall +answer
|
|
```
|
|
|
|
```
|
|
wikipedia.org. 130 IN A 185.15.58.224
|
|
```
|
|
|
|
One thing to know before you try other names: an entry in a hosts list blocks
|
|
exactly the name it names. `doubleclick.net` is on this list, and so are
|
|
`ad.doubleclick.net` and `www.google-analytics.com`. `ads.doubleclick.net` is
|
|
not on it, and nxdns does not block it — the entry for the parent says nothing
|
|
about the child. Two things do walk up the parent chain, and neither is in play
|
|
here: a rule you write yourself, with a wildcard pattern such as
|
|
`*.doubleclick.net`, and an Adblock Plus list's `||doubleclick.net^`, which
|
|
covers the name and everything under it. This list is a hosts file, so when you
|
|
pick a domain to test against it, pick one that is literally in the file.
|
|
|
|
## 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
|
|
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.)
|
|
|
|
## 13. Stop it
|
|
|
|
In the first terminal, press Ctrl-C.
|
|
|
|
```
|
|
info(nxdns): shutting down
|
|
```
|
|
|
|
nxdns catches SIGINT and SIGTERM, stops serving and exits 0.
|
|
|
|
Start it again with the same command as in step 6 and read the first log lines:
|
|
|
|
```
|
|
info(blocklist_manager): blocklist snapshot generation 1: 1 of 1 sources loaded, 2523973 bytes
|
|
info(nxdns): authority: database
|
|
```
|
|
|
|
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, 231 bytes
|
|
info(nxdns): authority: file (/home/you/nxdns-tutorial/config.zon)
|
|
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(blocklist_manager): pruned orphaned blocklist file 1.allow
|
|
info(blocklist_manager): pruned orphaned blocklist file 1.wild
|
|
info(blocklist_manager): pruned orphaned blocklist file 1.list
|
|
```
|
|
|
|
**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 three `pruned` lines are the rest of that removal: with the row gone, the
|
|
compiled files it owned belong to nobody, so the sweep that runs at every start
|
|
deletes them. The three names are the three bodies one source compiles into —
|
|
exact domains, wildcards, and the exceptions an Adblock Plus list can lift. 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 98000 domains
|
|
attached to the default group, a query log, and a web interface — all inside one
|
|
directory you can delete:
|
|
|
|
```sh
|
|
rm -rf ~/nxdns-tutorial
|
|
```
|
|
|
|
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
|
|
|
|
- [how-to/install-with-systemd.md](../how-to/install-with-systemd.md) — the same
|
|
thing as a real service on port 53, including a Raspberry Pi 5.
|
|
- [how-to/set-up-admin-authentication.md](../how-to/set-up-admin-authentication.md)
|
|
— put a password on the web interface before it leaves localhost.
|
|
- [reference/configuration.md](../reference/configuration.md) — every field you
|
|
did not set.
|
|
- [explanation/configuration-model.md](../explanation/configuration-model.md) —
|
|
why there are two authority modes and what each one is for.
|