309 lines
11 KiB
Markdown
309 lines
11 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. The only thing substituted during that run
|
|
was the tutorial directory.
|
|
|
|
## 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 10 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.
|
|
|
|
## 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. Start the server
|
|
|
|
In your first terminal:
|
|
|
|
```sh
|
|
zig-out/bin/nxdns run --data-dir ~/nxdns-tutorial/data --config ~/nxdns-tutorial/config.zon
|
|
```
|
|
|
|
```
|
|
info(migrations): config.db migrated from schema version 0 to 2
|
|
info(config_bootstrap): seeded the database from '/home/you/nxdns-tutorial/config.zon'
|
|
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): nxdns 0.1.0-dev 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`.
|
|
|
|
Leave this terminal running and switch to the second one.
|
|
|
|
## 6. Resolve a name
|
|
|
|
```sh
|
|
dig @127.0.0.1 -p 15353 example.com A +noall +answer
|
|
```
|
|
|
|
```
|
|
example.com. 90 IN A 172.66.147.243
|
|
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
|
|
said `0 of 0 sources loaded`.
|
|
|
|
## 7. 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.
|
|
|
|
## 8. 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]}
|
|
```
|
|
|
|
## 9. 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":1785683777,"last_success":1785683777,"url":"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts","last_error":"","domains":99277,"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
|
|
```
|
|
|
|
`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.
|
|
|
|
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
|
|
|
|
```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. 17 IN A 185.15.58.224
|
|
```
|
|
|
|
One thing to know before you try other names: a blocklist entry blocks exactly
|
|
the name it names. `doubleclick.net` on the list does not block
|
|
`ads.doubleclick.net`; that name is blocked because the list happens to contain
|
|
it too. Blocklist entries do not walk up the parent chain — only rules you write
|
|
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
|
|
|
|
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.)
|
|
|
|
## 12. 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 5 and read the first log line:
|
|
|
|
```
|
|
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
|
|
```
|
|
|
|
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`.
|
|
|
|
Press Ctrl-C again to stop this second process. Nothing is listening on 15353 or
|
|
8080 now, and nothing of nxdns is running.
|
|
|
|
## What you have now
|
|
|
|
A resolver that answers real queries, a real blocklist of about 99000 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 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.
|
|
|
|
## 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 the file seeds and the database rules.
|