The GatewayDNS server daemon: a single static binary that serves DNS for a network. Install it on a VPS, a home server, a Raspberry Pi or in a container, point your router's DHCP at it, and it caches and filters for everything behind it.
It is one of three consumers of GatewayDNS Core, the engine. The other two are anyone embedding the library directly, and GatewayDNS Desktop.
It resolves, filters and installs.
$ gatewaydnsd serve -config /etc/gatewaydns/gatewaydnsd.json
gatewaydnsd listening on 0.0.0.0:53
$ dig @127.0.0.1 example.com +short
172.66.147.243
$ dig @127.0.0.1 tracker.doubleclick.net
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAINWorking today: UDP and TCP listeners; upstreams over DoH, DoT, TCP or UDP with
failover between them; caching with negative caching and optional serve-stale;
blocklists and allowlists in hosts, domain-list and Adblock formats; a
token-authenticated management API with an optional query log; SIGHUP list
reload; graceful shutdown that finishes queries already accepted; and packages
for Debian, RPM and Alpine plus a container image.
Not yet: per-device policy from a device table, a Prometheus metrics endpoint, persistent query-log storage, and list auto-refresh.
Download the package for your architecture from the releases page, then:
sudo dpkg -i gatewaydnsd_1.0.0_amd64.deb # or: rpm -i, apk add --allow-untrusted
sudo $EDITOR /etc/gatewaydns/gatewaydnsd.json
sudo gatewaydnsd check -config /etc/gatewaydns/gatewaydnsd.json
sudo systemctl enable --now gatewaydnsd # or: rc-update add gatewaydnsd && rc-service gatewaydnsd startThe package does not start the service for you. Port 53 is nearly always
already held — by systemd-resolved on a Debian desktop, by dnsmasq on a
router — and a package that seized it on install would turn "I installed a DNS
server" into "my machine lost DNS". Free the port first:
sudo systemctl disable --now systemd-resolved
printf 'nameserver 127.0.0.1\n' | sudo tee /etc/resolv.confThe service runs as an unprivileged gatewaydns user with
CAP_NET_BIND_SERVICE for port 53 and a systemd sandbox
(the unit is worth reading before you
trust it with your network).
docker run -d --name gatewaydns \
-p 53:53/udp -p 53:53/tcp \
--cap-add NET_BIND_SERVICE \
-v /etc/gatewaydns:/etc/gatewaydns:ro \
ghcr.io/daboss2003/dnsd:latestThe image is scratch plus the binary and a CA bundle: no shell, no package
manager, no libc. It runs as uid 65532. If you would rather not grant
NET_BIND_SERVICE, set listen.addr to 0.0.0.0:5353 and publish it as
-p 53:5353/udp.
make build # a static binary in ./gatewaydnsd
make dist # every release target, into ./dist
make packages # .deb, .rpm and .apk (needs nfpm)
make docker # the container imageThis module depends on the engine by version, like any other dependency, so it builds on its own with nothing else checked out.
To develop the two together, check them out side by side and add a go.work
naming both — it is deliberately not committed, because a workspace in a
repository silently overrides every collaborator's module resolution:
go work init ./dns ./dnsd ./dns-desktopgatewaydnsd config-default prints the full schema with every default filled
in, and gatewaydnsd check -config FILE validates a file and reports every
problem it finds rather than the first. Both are generated from the same code
the daemon runs, so neither can drift from it.
Durations are written the way people write them: "5s", "250ms", "24h".
| Key | Default | Meaning |
|---|---|---|
addr |
127.0.0.1:5353 |
Address to bind. Port 53 is privileged. |
udp / tcp |
true |
Either may be disabled; both cannot. A client that receives a truncated UDP answer retries over TCP, so disabling TCP makes every large answer unresolvable. |
max_in_flight |
2048 |
Concurrent queries before new ones are shed. |
max_tcp_conns |
512 |
Concurrent TCP connections. |
query_timeout |
5s |
Bound on answering one query. |
| Key | Default | Meaning |
|---|---|---|
servers |
tls://1.1.1.1:853, tls://9.9.9.9:853 |
Endpoints in preference order, each scheme://address where scheme is udp, tcp, tls or https. A udp:// upstream is automatically paired with TCP to the same address. |
strategy |
sequential |
sequential, race, round-robin or random. race is faster at the tail and leaks every name to every upstream raced, which is a privacy decision rather than a tuning one. |
randomize |
false |
DNS-0x20 case randomisation. Adds entropy against off-path forgery; a small number of servers normalise case in replies and break outright with it on. |
timeout |
2s |
Bound on one upstream exchange. |
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
|
max_entries |
10000 |
|
min_ttl / max_ttl |
0s / 24h |
Clamp how long answers are held. |
serve_stale |
0s |
Answer from expired entries for this long when no upstream can be reached (RFC 8767). Off unless asked for: it is a deliberate correctness trade. |
| Key | Default | Meaning |
|---|---|---|
blocklists |
none | Paths to list files, in hosts, domain-list or Adblock format. Remote URLs are not fetched: a resolver that cannot start because a list server is down is worse than one with a stale list. |
allowlists |
none | An allow rule beats a block rule whatever their specificity, which is how anyone recovers from a false positive in a list of a million names nobody can audit. |
block |
nxdomain |
What a filtered client is told: nxdomain, refused or nodata. Every mode carries an RFC 8914 Extended DNS Error saying the name was filtered, so a client can tell "your gateway blocked this" from "no such name". |
SIGHUP, or systemctl reload gatewaydnsd, re-reads the lists without dropping
a query. A reload that fails leaves the previous rules in force. Changing
anything else — an address, an upstream — needs a restart, because it means new
sockets and a new resolver however it is dressed up.
Off unless configured, and worth understanding before turning on. It reads the query log and changes the filtering policy, so it is the most dangerous surface in the product.
| Key | Default | Meaning |
|---|---|---|
addr |
none | Address to serve on. Put it on loopback, or on an interface that is not the one answering DNS. Empty means the API does not exist, which is the smallest attack surface available. |
token |
none | Bearer token, 32 characters minimum. Required. Compared in constant time. |
token_file |
none | Path to read the token from instead. Mode 0600, owned by the gatewaydns user. Trailing newlines are trimmed. |
expose_log |
false |
Enables /api/v1/queries*, which returns a record of every name every device on the network looked up. Gated separately from everything else for that reason. |
read_only |
false |
Refuses every mutating request. |
allowed_origins |
none | CORS origins, named explicitly. * is refused. |
max_page_size |
1000 |
Rows one query-log request may read. |
log_retention |
7 days / 1,000,000 | {"max_age": "24h", "max_entries": 50000}. How much browsing history is kept in memory. |
Endpoints: GET /healthz (unauthenticated, says nothing about the network),
GET /api/v1/stats, GET /api/v1/cache, POST /api/v1/cache/flush,
GET /api/v1/policy, POST /api/v1/policy/reload, and with expose_log,
GET /api/v1/queries, /api/v1/queries/count and /api/v1/queries/top.
Every failure is JSON with an error field, and every path under /api/v1
answers 401 to a caller with no credential — including paths that do not
exist, so the route table is not an oracle.
| Key | Default | Meaning |
|---|---|---|
level |
INFO |
DEBUG, INFO, WARN, ERROR. |
format |
text |
text, json or syslog. |
| Signal | Effect |
|---|---|
SIGHUP |
Re-read the blocklists and allowlists. No query is dropped. |
SIGINT, SIGTERM |
Graceful shutdown: stop accepting, finish what was accepted, then exit. |
The engine has zero external dependencies and that is a promise to everyone who
embeds it. A daemon has good reasons to want dependencies the engine must never
have — a YAML parser so operators can write config files with comments, a
Prometheus client so /metrics is the format every monitoring stack already
scrapes. Keeping them apart means neither has to argue with the other.
See ADR 0015 in the engine repository.
Apache-2.0. See LICENSE.