This is a hardened fork of networkservicemesh/fanout.
Why this fork exists:
- Supply-chain security — The original project at
github.com/networkservicemesh/fanoutpulls in transitive dependencies from thegithub.com/networkservicemesh/*ecosystem. This fork eliminates those dependencies, reducing the attack surface to the minimum required set: CoreDNS,miekg/dns, andquic-go. - Robustness — Additional hardening such as connection pooling with liveness checks, retry-on-stream-failure, strict TLS version enforcement, and comprehensive race-detector-enabled tests.
- Modern encrypted DNS protocols — Full support for six DNS transport protocols, including DoH (HTTP/2), DoH3 (HTTP/3 over QUIC), and DoQ (DNS-over-QUIC, RFC 9250), in addition to the plain UDP, TCP, and DoT transports from the original.
fanout — parallel proxying DNS messages to upstream resolvers.
Each incoming DNS query that hits the CoreDNS fanout plugin will be replicated in parallel to each listed upstream. The first non-negative response from any of the queried DNS servers will be forwarded as a response to the application's DNS request.
| Protocol | RFC | Prefix / Directive | Default Port | Transport |
|---|---|---|---|---|
| DNS/UDP | — | (plain address) | 53 | UDP |
| DNS/TCP | — | (plain address) + network TCP |
53 | TCP |
| DoT (DNS-over-TLS) | RFC 7858 | tls:// or tls directive |
853/TCP | TLS over TCP |
| DoH (DNS-over-HTTPS) | RFC 8484 | https:// |
443/TCP | HTTP/2 over TLS |
| DoH3 (DNS-over-HTTPS/3) | RFC 8484 + RFC 9114 | h3:// |
443/UDP | HTTP/3 over QUIC |
| DoQ (DNS-over-QUIC) | RFC 9250 | quic:// |
853/UDP | QUIC (TLS 1.3, ALPN doq) |
Upstream addresses are distinguished by their URL prefix:
fanout . <plain-host>[:port] # UDP/TCP (default)
fanout . tls://<host>[:port] # DoT
fanout . https://<host>/<path> # DoH (HTTP/2)
fanout . h3://<host>/<path> # DoH3 (HTTP/3 over QUIC)
fanout . quic://<host>[:port] # DoQ (RFC 9250, default port 853)
You can mix protocols freely in a single fanout block. The first successful response wins, regardless of which transport delivered it.
fanout FROM TO... {
tls CERT KEY CA
tls_servername NAME
network PROTOCOL
worker-count COUNT
policy POLICY
weighted-random-server-count COUNT
weighted-random-load-factor FACTOR...
except DOMAIN...
except-file FILE
attempt-count COUNT
timeout DURATION
debug
race
race-continue-on-error
}
tlsCERT KEY CA — define the TLS properties for TLS connections. From 0 to 3 arguments can be provided with the meaning as described below:tls— no client authentication is used, and the system CAs are used to verify the server certificatetlsCA — no client authentication is used, and the file CA is used to verify the server certificatetlsCERT KEY — client authentication is used with the specified cert/key pair. The server certificate is verified with the system CAstlsCERT KEY CA — client authentication is used with the specified cert/key pair. The server certificate is verified using the specified CA file
tls_servernameNAME — allows you to set a server name in the TLS configuration; for instance9.9.9.9needs this to be set todns.quad9.net. Multiple upstreams are still allowed in this scenario, but they have to use the sametls_servername. E.g. mixing9.9.9.9(Quad9) with1.1.1.1(Cloudflare) will not work.worker-count— the number of parallel queries per request. By default equals the count of the upstream list. Use this only to reduce parallel queries per request.policy— specifies the policy for DNS server selection. The default issequential.sequential— select DNS servers one-by-one based on their orderweighted-random— select DNS servers randomly based onweighted-random-server-countandweighted-random-load-factor
weighted-random-server-count— the number of DNS servers to be queried. Equals the number of specified upstreams by default. Used only with theweighted-randompolicy.weighted-random-load-factor— the probability of selecting a server (1–100). Specified in the order of the upstream list. Default is 100 for all servers. Used only with theweighted-randompolicy.network— specific network protocol for plain upstreams:tcp,udp(default), ortcp-tls.except— a space-separated list of domains to exclude from proxying.except-file— path to a file with line-separated domains to exclude from proxying.attempt-count— the number of failed attempts before considering an upstream to be down. If0, the upstream will never be marked as down and the request will run untiltimeout. Default is3.timeout— the maximum time for the entire request. Default is30s.debug— emit per-upstream intermediate request failures through thefanoutlogger so failed attempts are visible even when another upstream still answers successfully.race— gives priority to the first result, whether it is negative or not, as long as it is a valid DNS response.race-continue-on-error— When enabled together withrace, fanout does not early-return on erroneous DNS responses such asSERVFAIL, but still treatsNOERRORandNXDOMAINas terminal answers that can end the race immediately. The default isfalse.
If monitoring is enabled (via the prometheus plugin) then the following metrics are exported:
coredns_fanout_request_count_total{to}— request attempt count per upstream, including attempts that fail before a DNS response is received.coredns_fanout_request_error_count_total{to, error}— failed request attempt count per upstream and error class.coredns_fanout_response_rcode_count_total{to, rcode}— count of RCODEs per upstream (i.e., successf).coredns_fanout_request_duration_seconds{to}— duration of request attempts that completed with a valid DNS response.
Where to is one of the upstream servers (TO from the config), rcode is the returned RCODE
from the upstream, and error is one of the bounded classes used by fanout
(for example connect_failed, request_send_failed, response_read_failed, or response_decode_failed).
Proxy all requests within example.org. to four nameservers. The first positive response wins.
example.org {
fanout . 127.0.0.1:9005 127.0.0.1:9006 127.0.0.1:9007 127.0.0.1:9008
}
Send parallel requests to three resolvers via TCP.
. {
fanout . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 {
network TCP
}
}
Proxy all requests to Quad9 using DNS-over-TLS (RFC 7858).
The tls_servername is mandatory because 9.9.9.9 can't be used in TLS negotiation.
. {
fanout . tls://9.9.9.9 {
tls_servername dns.quad9.net
}
}
Proxy all requests to Cloudflare via DNS-over-HTTPS (RFC 8484, HTTP/2).
. {
fanout . https://cloudflare-dns.com/dns-query
}
Proxy all requests to Cloudflare via DNS-over-HTTPS over HTTP/3 (QUIC).
Use the h3:// prefix — it is internally converted to an HTTPS URL.
. {
fanout . h3://cloudflare-dns.com/dns-query
}
Proxy all requests to AdGuard DNS via DNS-over-QUIC (RFC 9250).
The default port is 853/UDP. TLS 1.3 with ALPN token doq is enforced automatically.
. {
fanout . quic://dns.adguard-dns.com
}
Fan out to multiple upstreams across different transports simultaneously. The first successful response from any transport wins.
. {
fanout . 1.1.1.1 https://cloudflare-dns.com/dns-query h3://cloudflare-dns.com/dns-query quic://dns.adguard-dns.com:853
}
Proxy everything except requests to example.org.
. {
fanout . 10.0.0.10:1234 {
except example.org
}
}
Send parallel requests to five resolvers but limit to two concurrent workers.
. {
fanout . 10.0.0.10:53 10.0.0.11:53 10.0.0.12:53 10.0.0.13:1053 10.0.0.14:1053 {
worker-count 2
}
}
Multiple upstreams are configured but one of them is down. With race enabled, the first result is returned immediately instead of waiting for timeouts.
. {
fanout . 10.0.0.10:53 10.0.0.11:53 10.0.0.12:53 {
race
}
}
By default race will return the first DNS response that arrives as long as it is a
valid DNS result — this may be a non-success RCODE such as SERVFAIL or NXDOMAIN.
If you prefer to keep the latency benefits of race but avoid early-returning on upstream error responses,
enable race-continue-on-error. When both race and race-continue-on-error are set, fanout will
still end the race immediately for NOERROR and NXDOMAIN, but it will keep waiting when a fast
upstream returns an error such as SERVFAIL.
Example: A fast upstream returns SERVFAIL and a slow upstream returns NOERROR. With
race alone the user receives SERVFAIL; with race and race-continue-on-error set the
user receives a successful domain name resolution (if it arrives before the request timeout).
. {
fanout . 10.0.0.10:53 10.0.0.11:53 10.0.0.12:53 {
race
race-continue-on-error
}
}
Send parallel requests to two randomly selected resolvers. 127.0.0.1:9007 is selected most frequently due to its highest load factor.
example.org {
fanout . 127.0.0.1:9005 127.0.0.1:9006 127.0.0.1:9007 {
policy weighted-random
weighted-random-server-count 2
weighted-random-load-factor 50 70 100
}
}
See coredns/README.md for instructions on building a CoreDNS binary or Docker image with this plugin.