Quick start
goethite is pre-alpha. It forwards queries to the upstream resolvers you configure, over DNS over TLS, DNS over HTTPS or plain DNS, with failover, caches the answers, and blocks names from filter lists (see Filtering).
Prerequisites
Section titled “Prerequisites”- Rust, installed with rustup. The repository pins its toolchain in
rust-toolchain.toml, so rustup installs the right version on the first build. - dig (from BIND's
dnsutils/bind-toolspackage) to send test queries.
Build and run
Section titled “Build and run”git clone https://github.com/nxplain-sh/goethite && cd goethite
cargo run -- run --config config/goethite.example.tomlThe example config binds to 127.0.0.1:15353, so the development server runs without root.
(Port 5353 is avoided because multicast DNS already uses it on most desktops.)
Production deployments use port 53.
It forwards to Quad9 (9.9.9.9, then 149.112.112.112) over DNS over TLS.
goethite never picks an upstream for you: the config must list at least one [[upstream]]:
[[upstream]]
address = "9.9.9.9" # an IP address; the port defaults to the protocol's
protocol = "tls" # "tls", "https", "udp" or "tcp"
tls_name = "dns.quad9.net" # the name the certificate must be valid for
[[upstream]]
address = "9.9.9.9"
protocol = "https"
url = "https://dns.quad9.net/dns-query"Certificates are checked against the Mozilla root certificates built into goethite.
Check a config
Section titled “Check a config”check-config reads a config file, builds the upstreams and compiles the filter lists as run
would, without binding a socket or downloading anything, and exits with status 1 if something is
wrong:
cargo run -- check-config --config config/goethite.example.tomlIt is stricter than startup about filter lists: at startup a list file that cannot be read is
logged and skipped so that resolution keeps working, while check-config reports it as an error.
Rules written into the config itself ([filter] rules) are always checked strictly; a rule
goethite does not support stops the server from starting, since it is most likely a typo.
Query it
Section titled “Query it”In another terminal:
dig @127.0.0.1 -p 15353 goethite.testThe answer section contains 127.0.0.53:
;; ANSWER SECTION:
goethite.test. 60 IN A 127.0.0.53goethite.test is answered by goethite itself, which makes it a handy liveness check. Every
other name is forwarded upstream:
dig @127.0.0.1 -p 15353 example.comThe header shows ra (recursion available) and the answer comes from Quad9, over an encrypted
connection:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4242
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1Ask again and the answer comes from goethite's cache: the query time drops to about 0 ms and the
TTL has counted down. The [cache] table in the config sets its size and TTL limits.
The same queries work over TCP:
dig @127.0.0.1 -p 15353 +tcp example.comWatch it
Section titled “Watch it”The terminal UI shows the dashboard, the query log and the filter lists:
cargo run -- tuiThe web UI shows the same in a browser at http://127.0.0.1:8053/. It is built
separately with Node.js 22.19 or later (CI and release builds use 24); build it once, before or
while goethite runs:
cd web && npm ci --ignore-scripts && npm run buildStop it
Section titled “Stop it”Press Ctrl+C (or send SIGTERM). goethite stops accepting new queries,
finishes answering queries already in progress (for up to 5 seconds), closes open TCP
connections, and exits.