feat: add DNS-over-TLS (DoT) listener #25
Reference in New Issue
Block a user
Delete Branch "feat/dns-over-tls"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds DNS-over-TLS (RFC 7858) listener to numa, with the protocol-layer hardening and config/test consolidation that shipped alongside it.
Core feature:
src/dot.rs— TLS listener on port 853 with persistent connections, coalesced length-prefix + response writes, configurable bind addr/port[dot]config section:enabled,port,bind_addr, optionalcert_path/key_pathwith self-signed CA fallbackhandle_query→ transport-agnosticresolve_queryso UDP and DoT share the same resolver pipeline (zero-alloc on the UDP hot path)Protocol hardening:
"dot"advertised in TLS ServerHello (RFC 7858bis §3.2) — rustls enforces strictness, rejecting handshakes with mismatched ALPN as a cross-protocol confusion defense (verified bydot_rejects_non_dot_alpn){tld}.{tld}SAN on the self-signed DoT cert, since strict TLS clients reject wildcards under single-label TLDs (verified empirically: kdig with a wildcard-only cert fails the handshake; adding the explicit SAN makes it succeed)WRITE_TIMEOUT = 10sonwrite_framedto prevent slow-reader DoSHANDSHAKE_TIMEOUT = 10sagainst slowloris on the TLS handshakeMAX_CONNECTIONS = 512semaphore with 100ms backoff on accept errorsTimeouts, limits, error handling:
send_responsehelper unifying error-response serializationBug fix caught by Suite 6 integration testing:
load_tls_configwas missingrustls::crypto::ring::default_provider().install_default(), causing numa to panic on DoT startup when[dot] cert_path/key_pathwas set AND[proxy] enabled = false. The proxy'sbuild_tls_confignormally installs the provider as a side effect, masking the gap. Exactly the deployment shape for "numa as DoT-only server" would have hit this.Config consolidation:
data_diris now a[server]TOML field instead of a hardcoded path, threaded intobuild_tls_configvia explicit parameter. Tests and containerized deploys override it without env var injection.HOME,SUDO_USER) and standard ecosystem conventions (RUST_LOG).Infrastructure:
DockerfilenowEXPOSE 853/tcpsodocker run -p 853:853works out of the boxnuma.tomlexample documents the[dot]section and[server] data_diroverrideTesting
Unit tests (
cargo test) — 127 passing, +6 DoT-specific:dot_resolves_local_zone— zone lookup over TLSdot_multiple_queries_on_persistent_connection— connection reusedot_nxdomain_for_unknown— SERVFAIL propagation via blackhole upstreamdot_concurrent_connections— semaphore + concurrent handshakesdot_negotiates_alpn— ALPN"dot"advertisement verified viaconn.alpn_protocol()dot_rejects_non_dot_alpn— rustls rejects mismatched ALPN (cross-protocol defense)Integration tests (
./tests/integration.sh) — 2 new suites:+keepopen), ALPN positive + negative viaopenssl s_clientCross-implementation empirical verification:
numa setup-phonemobileconfig) — real iPhone resolving real queries over DoT with persistent connections observed in the logKnown gaps (follow-up work)
Intentionally out of scope for this PR:
MAX_CONNECTIONScap. A single source can starve the connection pool.CancellationTokenacross all subsystems.Each has a dedicated implementation sketch from the design discussion; none block correctness for common deployments (home/office DNS resolver with trusted clients).
🤖 Generated with Claude Code