coredns-plugins
External CoreDNS plugins for encrypted-DNS discovery, multi-certificate TLS,
DNS Cookies, RFC 2136 dynamic update and DNS measurement, plus a Dagger pipeline
that builds a CoreDNS binary with them compiled in.
| Plugin |
What it does |
Status |
sni_tls |
Replaces the stock tls directive with SNI-based certificate selection, so one listener can serve several hostnames whose certs come from different issuers |
Implemented, tested, running in production |
radnr |
Advertises an encrypted DNS resolver on the LAN via the RFC 9463 DNR option in IPv6 Router Advertisements, plus optional RFC 8106 RDNSS |
Wire/protocol layer complete and RFC-conformant; running in production |
cookie |
DNS Cookies (RFC 7873) with the interoperable SipHash-2-4 server cookie of RFC 9018: validates and issues cookies, BADCOOKIE in enforce mode, three-stage secret rollover, per-case counters |
Implemented and tested; protocol rules modelled in TLA+ and Lean. Not yet deployed anywhere |
dynupdate |
Serves a zone from memory and accepts RFC 2136 dynamic updates for it, e.g. from an ACME client's DNS-01 solver, with a mutable type allow-list |
Implemented and tested; update rules proved in Lean. In-memory only, see its README |
probe |
A per-visitor synthetic measurement zone that records what each resolver's query reveals and can answer wrong on purpose (DNSSEC failure modes), plus an RFC 9567 error-report receiver |
Implemented and tested |
sni_tls and radnr run in production in the author's homelab (as
coredns-radnr, serving real LAN DNS over DoT/DoH/DoQ). They have real-world
mileage now, but only in one deployment and one topology — treat "works here"
accordingly. The other plugins are covered by tests, and by the models
and proofs in verification/, but a model checks the
model, not the deployment.
The Dagger pipeline can also compile in third-party plugins that don't live
in this repo, pinned by module version. Currently: upstream's
coredns/rrl for response rate limiting —
adopted rather than reimplemented, with the verification results and three
operational traps written up in docs/rrl-plugin.md.
Why these exist
sni_tls — CoreDNS's stock tls plugin loads exactly one cert/key pair per
listener, with no SNI-based selection. If you serve both a public name and an
internal-only name over DoT/DoH/DoQ, those two names need certificates from two
different issuers (a public ACME CA cannot sign .arpa or .internal), and the
stock plugin cannot put both on one tls:// block. The usual workaround is a
second CoreDNS instance with its own address, cert, and copy of the zone data.
sni_tls collapses that back into one instance. Design rationale, rejected
alternatives, and the verified-DDR caveat: docs/sni-tls-plugin.md.
radnr — RFC 9463 defines a DHCP/RA option that hands clients an encrypted
resolver's authentication domain name directly, so DoT/DoH/DoQ works with no
per-device configuration. Support exists client-side (systemd-resolved ≥ 256,
Windows 11) but router-side emitters are scarce. This plugin makes CoreDNS one.
cookie — CoreDNS has no DNS Cookie support: up to v1.14.6 it copies a
request's COOKIE option into the response unvalidated, which RFC 7873 §5.2 does
not allow, and upstream issue #6187 asking for real support has been open since
2023. This plugin issues and validates RFC 9018 server cookies so a cookie-bearing
client can prove its source address; it is a signal and an enforce mode, not
access control, because a client that sends no cookie cannot be made to. Design,
limits and what each rule is checked by: docs/cookie-plugin.md.
Building
Plugins are compiled into CoreDNS — there is no runtime plugin loading. Each
plugin is its own Go module so it can be developed and tested standalone, then
pulled into a CoreDNS build with a go.mod replace directive.
Test a plugin on its own:
cd sni_tls && go test -race ./...
cd radnr && go test -race ./...
cd cookie && go test -race ./...
cd dynupdate && go test -race ./...
cd probe && go test -race ./...
Build a CoreDNS binary with all of them compiled in, via Dagger (from ci/):
dagger call test-plugin --source=.. --plugin-dir=sni_tls
dagger call lint-plugin --source=.. --plugin-dir=sni_tls
dagger call build-coredns --source=.. --coredns-version=1.14.6
dagger call containerize --source=.. --coredns-version=1.14.6 \
export --path=./coredns-plugins.tar
BuildCoredns clones the pinned upstream CoreDNS tag, patches plugin.cfg
(sni_tls replaces stock tls; radnr is inserted next to health; each
other plugin gets the slot its README and ci/main.go explain, cookie for
instance right after tsig and before cache), runs
go generate to regenerate zplugin.go, and builds. Containerize wraps the
binary in a distroless base and returns an OCI tarball — it does not push. See
ci/main.go for why pushing is left to the caller.
Two ordering constraints the pipeline encodes, both of which were real bugs:
go generate must run before go mod tidy. Tidy-first sees nothing
importing the replaced modules yet and strips the replace as unused.
plugin.cfg order is semantic, not just registration order. It determines
the order handlers run in; a plugin in the wrong slot can silently never see a
query.
Deploying
The container needs CAP_NET_BIND_SERVICE to bind port 53 as a non-root user
(the distroless base has no shell, so upstream's setcap approach isn't
available). On Kubernetes:
securityContext:
capabilities:
add: ["NET_BIND_SERVICE"]
radnr additionally needs host networking and CAP_NET_RAW to send Router
Advertisements.
radnr puts a second RA sender on your LAN. That can disrupt IPv6 for
every device on the segment. It defaults to RouterLifetime=0 (explicitly not
a default router) and refuses to advertise prefixes, and it has a dry-run
mode that logs what it would send and transmits nothing. Start there. Read
radnr/README.md before running it live.
Do not deploy sni_tls with more than one certificate on an instance
serving RFC 9462 verified DDR. An unmatched-SNI client could be served a
cert without the required IP-SAN, silently defeating verified discovery. The
caveat is explained in docs/sni-tls-plugin.md.
Testing
Every plugin is covered by unit tests, and sni_tls additionally has:
- a real loopback TLS 1.3 handshake asserting ML-KEM (post-quantum) negotiation
(
pqc_test.go)
- a fuzz target over SNI lookup (
fuzz_test.go)
- partial-cert-set tests: one cert source being absent must not take the other
listener down, since certs from different issuers rotate independently
radnr has byte-exact golden tests against the RFC 9463/9460/8415 encoding
rules, and covers Router Solicitation responses (RFC 4861 §6.2.6, rate-limited)
and interval jitter (§6.2.1).
cookie reproduces every server cookie RFC 9018 Appendix A prints, byte for
byte, checks the 1-hour/5-minute window across the 2106 timestamp wrap, runs a
request through the handler for each RFC 7873 §5.2 case in both modes and both
transports, and carries fuzz targets over option classification, cookie
verification and request handling.
Everything runs offline — GOPROXY=off go test ./... passes in every module.
Each plugin also carries benchmarks for its request path (bench_test.go);
docs/performance.md records where the time goes, what
was changed and what each change bought.
verification/ holds TLA+ models of the lifecycle and
concurrency paths (cert reload, caddy restart hooks, RA scheduling, update
atomicity, DNS Cookie secret rollout and client bootstrap) and Lean proofs of
the pure parts (the RFC 9463 option codec, SNI wildcard selection, RFC 1982
serials, RFC 2136 update rules, the DNS Cookie option shapes, timestamp window
and SipHash-2-4). CI runs both: verification/run.sh.
Contributing
See CONTRIBUTING.md.
Licence
MIT. CoreDNS itself is Apache-2.0; these plugins are separate works
compiled into it.