coredns-plugins
External CoreDNS plugins for encrypted-DNS discovery and multi-certificate TLS,
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 |
Both plugins 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 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.
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 ./...
Build a CoreDNS binary with both 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), 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
Both plugins are 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).
Everything runs offline — GOPROXY=off go test ./... passes in both modules.
Contributing
See CONTRIBUTING.md.
Licence
MIT. CoreDNS itself is Apache-2.0; these plugins are separate works
compiled into it.