- Scheme 77.5%
- Shell 13.6%
- Rust 5.9%
- Makefile 1.8%
- Python 0.8%
- Other 0.4%
| .builds | ||
| .claude | ||
| .jerboa | ||
| bench | ||
| bin | ||
| docs | ||
| fuzz | ||
| lib/jerboa-dns | ||
| scripts | ||
| static | ||
| support | ||
| tests | ||
| wasm | ||
| zones | ||
| .gitignore | ||
| .gitsafe.json | ||
| .jerbuild | ||
| .jerbuild-jdns-data | ||
| AGENTS.md | ||
| build.ss | ||
| jpkg.lock | ||
| jpkg.policy.sexp | ||
| jpkg.sexp | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| SECURITY.md | ||
| vs-djbdns.md | ||
| vs.md | ||
jerboa-dns
Authoritative-only DNS server and zone compiler in Jerboa/Chez Scheme.
jdns serves both UDP and TCP DNS on the configured port.
Build and Test
make build
make test
Release evidence:
make release-evidence
The release bundle records DNS soak/load status under
dist/release-evidence/soak/. By default this is a blocked, record-only status;
set JDNS_RUN_RELEASE_SOAK=1 with production-duration JDNS_SOAK_SECONDS to
capture current UDP dnsperf evidence plus bounded DNS-over-TCP smoke.
Bounded local coverage-guided fuzz evidence is recorded with:
JDNS_RUN_COVERAGE_FUZZ=1 JDNS_FUZZ_RUNS=2048 make fuzz-evidence
When JDNS_RUN_COVERAGE_FUZZ=1 is set for make release-evidence, the release
bundle also records coverage_fuzz_status=local-smoke-recorded under
dist/release-evidence/fuzz-evidence/ after dns_query, cdb_record, and
cdb_file complete.
Authoritative-only, amplification, and edge rate-limit policy is documented in
docs/rate-limit-amplification.md and copied into
dist/release-evidence/abuse-policy.txt.
Static release targets live behind the static-* Make targets, for example:
make static-freebsd
Runtime Security Defaults
jdns binds the UDP and TCP sockets first, then chroots to ROOT (default
.), drops supplementary groups with setgroups(0, NULL), applies
setgid/setuid, verifies the resulting identities, and then applies platform
filesystem restrictions. A root launch requires both a non-zero UID and
non-zero GID. Root retention is denied unless the development-only
JDNS_RETAIN_PRIVILEGES=1 / --retain-privileges override is explicit; that
override is audited in the log and is not a production configuration. See
docs/runtime-hardening.md.
TCP handling uses Jerboa fibers over nonblocking sockets when the fiber runtime is available before chroot. Embedded static builds can fall back to bounded nonblocking OS-thread tasks instead of failing at startup. TCP clients are capped at 128 concurrent sessions, and idle TCP reads/writes time out after 15 seconds.
The daemon is authoritative-only and does not recurse or forward queries.
Amplification-prone ANY queries are minimized with a small REFUSED response.
Production deployments must still provide edge rate limits until native
response-rate limiting exists.
Filesystem sandbox setup fails closed by default. For local development only, you can allow a weaker fallback with either:
JDNS_ALLOW_SANDBOX_FALLBACK=1 jdns ...
jdns --allow-sandbox-fallback ...
The WASM DNS parser and WASM CDB reader are embedded into the binary at build
time (lib/jerboa-dns/wasm-embedded.ss, regenerated by wasm/build.sh), so
the sandbox is active with no files on disk, including inside the chroot.
If the wasmi runtime itself is not linked in, startup/query handling fails
closed by default. The only in-process parser fallback is the explicit
development opt-out JDNS_ALLOW_IN_PROCESS_PARSER=1 or
JDNS_ALLOW_IN_PROCESS_CDB=1.
JDNS_PARSER_WASM / JDNS_CDB_WASM point at on-disk modules as an explicit
development override (useful for testing a rebuilt parser without relinking);
they are not needed in production.
Zone Updates
jdns-data writes a temporary CDB and atomically renames it into place. The
server validates one immutable in-memory snapshot at startup. A low-frequency
watcher detects device/inode/time/size changes, builds and validates a candidate
off the request path, and atomically swaps it; in-flight queries finish against
their old snapshot. Queries never reopen the CDB. The default one-second poll
can be changed with JDNS_CDB_RELOAD_MS (100–60000 ms), and malformed updates
leave the last valid snapshot serving.
Zone Formats
jdns-data accepts the original tinydns data format and a grouped
s-expression format. The s-expression form starts with (zone ...), so the
compiler auto-detects it:
(zone
(domain "example.com"
(soa "ns1.example.com" "1.2.3.4" 259200)
(ns "ns2.example.com" "1.2.3.5" 259200)
(a "1.2.3.4" 86400)
(mx "mail.example.com" 10 "1.2.3.6" 86400)
(txt "v=spf1 include:example.net mx -all" 86400)
(txt "_dmarc" "v=DMARC1; p=none" 86400))
(domain "www.example.com"
(a+ptr "1.2.3.4" 86400))
(domain "static.example.com"
(cname "www.example.com" 3600))
(domain "ipv6.example.com"
(aaaa "2001:db8::1" 86400)))
TXT records can optionally start with a relative owner name. For example,
(txt "_dmarc" "v=DMARC1; p=none") inside example.com compiles as a TXT
record for _dmarc.example.com.
Compile either format the same way:
make jdns-data ARGS="zones/example.sexp data.cdb"