fanout

package module
v1.13.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 21, 2026 License: Apache-2.0 Imports: 40 Imported by: 0

README

fanout

Go Report Card CI Vulnerabilities

About This Fork

This is a hardened fork of networkservicemesh/fanout.

Why this fork exists:

  1. Supply-chain security — The original project at github.com/networkservicemesh/fanout pulls in transitive dependencies from the github.com/networkservicemesh/* ecosystem. This fork eliminates those dependencies, reducing the attack surface to the minimum required set: CoreDNS, miekg/dns, and quic-go.
  2. Robustness — Additional hardening such as connection pooling with liveness checks, retry-on-stream-failure, strict TLS version enforcement, and comprehensive race-detector-enabled tests.
  3. 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.

Name

fanout — parallel proxying DNS messages to upstream resolvers.

Description

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.

Supported Protocols

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.

Syntax

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
    race
    race-continue-on-error-response
}
  • tls CERT 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 certificate
    • tls CA — no client authentication is used, and the file CA is used to verify the server certificate
    • tls CERT KEY — client authentication is used with the specified cert/key pair. The server certificate is verified with the system CAs
    • tls CERT KEY CA — client authentication is used with the specified cert/key pair. The server certificate is verified using the specified CA file
  • tls_servername NAME — allows you to set a server name in the TLS configuration; for instance 9.9.9.9 needs this to be set to dns.quad9.net. Multiple upstreams are still allowed in this scenario, but they have to use the same tls_servername. E.g. mixing 9.9.9.9 (Quad9) with 1.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 is sequential.
    • sequential — select DNS servers one-by-one based on their order
    • weighted-random — select DNS servers randomly based on weighted-random-server-count and weighted-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 the weighted-random policy.
  • 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 the weighted-random policy.
  • network — specific network protocol for plain upstreams: tcp, udp (default), or tcp-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. If 0, the upstream will never be marked as down and the request will run until timeout. Default is 3.
  • timeout — the maximum time for the entire request. Default is 30s.
  • 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-response — When enabled together with race, fanout does not early-return on non-success DNS responses (for example SERVFAIL or NXDOMAIN) and only early-returns on RcodeSuccess. The default is false.

Metrics

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).

Examples

Plain DNS (UDP)

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
}
Plain DNS (TCP)

Send parallel requests to three resolvers via TCP.

. {
    fanout . 10.0.0.10:53 10.0.0.11:1053 [2003::1]:53 {
        network TCP
    }
}
DNS-over-TLS (DoT)

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
    }
}
DNS-over-HTTPS (DoH)

Proxy all requests to Cloudflare via DNS-over-HTTPS (RFC 8484, HTTP/2).

. {
    fanout . https://cloudflare-dns.com/dns-query
}
DNS-over-HTTPS/3 (DoH3)

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
}
DNS-over-QUIC (DoQ)

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
}
Mixed Protocols

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
}
Excluding Domains

Proxy everything except requests to example.org.

. {
    fanout . 10.0.0.10:1234 {
        except example.org
    }
}
Limiting Workers

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
    }
}
Race Mode

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 error responses, enable race-continue-on-error-response. When both race and race-continue-on-error-response are set, fanout will only early‑return for RcodeSuccess and will wait briefly for a successful response instead of immediately accepting a fast negative result.

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-response 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-response
    }
}
Weighted Random Selection

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
    }
}

Building CoreDNS with fanout

See coredns/README.md for instructions on building a CoreDNS binary or Docker image with this plugin.

Documentation

Overview

Package fanout - parallel proxying DNS messages to upstream resolvers.

Supported transport protocols:

  • DNS/UDP (plain, default)
  • DNS/TCP (plain)
  • DoT - DNS-over-TLS (RFC 7858) — tls:// prefix or "tls" directive
  • DoH - DNS-over-HTTPS (RFC 8484) — https:// prefix (HTTP/2 transport)
  • DoH3 - DNS-over-HTTPS (RFC 8484) — h3:// prefix (HTTP/3 / QUIC transport, RFC 9114)
  • DoQ - DNS-over-QUIC (RFC 9250) — quic:// prefix

Index

Constants

View Source
const (

	// TCPTLS net type for a DNS-over-TLS Client (DoT, RFC 7858).
	TCPTLS = "tcp-tls"
	// TCP net type for a Client (plain DNS over TCP).
	TCP = "tcp"
	// UDP net type for a Client (plain DNS over UDP).
	UDP = "udp"
	// DOH net type for a DNS-over-HTTPS Client (DoH, RFC 8484 over HTTP/2).
	DOH = "dns-over-https"
	// DOH3 net type for a DNS-over-HTTPS Client using HTTP/3 over QUIC (DoH3, RFC 8484 + RFC 9114).
	DOH3 = "dns-over-https3"
	// DOQ net type for a DNS-over-QUIC Client (DoQ, RFC 9250).
	DOQ = "dns-over-quic"
)

Variables

View Source
var (
	RequestCount = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "fanout",
		Name:      "request_count_total",
		Help:      "Number of request attempts started per upstream.",
	}, []string{"to"})
	ErrorCount = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "fanout",
		Name:      "request_error_count_total",
		Help:      "Number of failed request attempts per upstream, grouped by bounded error class.",
	}, []string{"error", "to"})
	RcodeCount = promauto.NewCounterVec(prometheus.CounterOpts{
		Namespace: plugin.Namespace,
		Subsystem: "fanout",
		Name:      "response_rcode_count_total",
		Help:      "Number of responses per response code per upstream.",
	}, []string{"rcode", "to"})
	RequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{
		Namespace: plugin.Namespace,
		Subsystem: "fanout",
		Name:      "request_duration_seconds",
		Buckets:   plugin.TimeBuckets,
		Help:      "Histogram of the time request attempts with a valid DNS response took.",
	}, []string{"to"})
)

Variables declared for monitoring.

Functions

This section is empty.

Types

type Client

type Client interface {
	Request(context.Context, *request.Request) (*dns.Msg, error)
	Endpoint() string
	Net() string
	SetTLSConfig(*tls.Config)
}

Client represents the proxy for remote DNS server

func NewClient

func NewClient(addr, net string) Client

NewClient creates new client with specific addr and network

func NewDoH3Client added in v1.12.0

func NewDoH3Client(endpoint string) Client

NewDoH3Client creates a new DNS-over-HTTPS client using HTTP/3 (QUIC) transport. The endpoint must be a full HTTPS URL (e.g. "https://dns.google/dns-query").

func NewDoHClient added in v1.12.0

func NewDoHClient(endpoint string) Client

NewDoHClient creates a new DNS-over-HTTPS client for the given endpoint URL. The endpoint must be a full URL (e.g. "https://dns.google/dns-query"). The client uses HTTP/2 with a connection-pooling transport for performance.

func NewDoQClient added in v1.12.0

func NewDoQClient(addr string) Client

NewDoQClient creates a new DNS-over-QUIC client for the given address. The address should be in host:port format (e.g. "dns.example.com:853").

type Domain

type Domain interface {
	Get(string) Domain
	AddString(string)
	Add(string, Domain)
	Contains(string) bool
	IsFinal() bool
	Finish()
}

Domain represents DNS domain name

func NewDomain

func NewDomain() Domain

NewDomain creates new domain instance

type Fanout

type Fanout struct {
	ExcludeDomains Domain

	Timeout                     time.Duration
	Race                        bool
	RaceContinueOnErrorResponse bool

	From string
	// Attempts is the number of times to retry a failed upstream request.
	// A value of 0 means infinite retries (bounded only by Timeout).
	Attempts    int
	WorkerCount int

	ServerSelectionPolicy policy
	TapPlugin             *dnstap.Dnstap
	Next                  plugin.Handler
	// contains filtered or unexported fields
}

Fanout represents a plugin instance that can do async requests to list of DNS servers.

func New

func New() *Fanout

New returns reference to new Fanout plugin instance with default configs.

func (*Fanout) AddClient

func (f *Fanout) AddClient(p Client)

AddClient is used to add a new DNS server to the fanout. It also increments WorkerCount and serverCount. For bulk initialization during setup, use addClient instead.

func (*Fanout) Name

func (f *Fanout) Name() string

Name implements plugin.Handler.

func (*Fanout) OnShutdown

func (f *Fanout) OnShutdown() error

OnShutdown stops all configured clients and releases their resources.

func (*Fanout) OnStartup

func (f *Fanout) OnStartup() (err error)

OnStartup starts a goroutines for all clients.

func (*Fanout) ServeDNS

func (f *Fanout) ServeDNS(ctx context.Context, w dns.ResponseWriter, m *dns.Msg) (int, error)

ServeDNS implements plugin.Handler.

type SequentialPolicy

type SequentialPolicy struct {
}

SequentialPolicy is used to select clients based on its sequential order

type Transport

type Transport interface {
	Dial(ctx context.Context, net string) (*dns.Conn, error)
	// Yield returns a healthy connection to the pool for reuse.
	// Only call this for connections that completed a successful request-response cycle.
	// For failed connections, call conn.Close() instead.
	Yield(conn *dns.Conn)
	SetTLSConfig(*tls.Config)
	// Close drains the connection pool and releases resources.
	Close()
}

Transport represent a solution to connect to remote DNS endpoint with specific network

func NewTransport

func NewTransport(addr string) Transport

NewTransport creates new transport with address

type WeightedPolicy

type WeightedPolicy struct {
	// contains filtered or unexported fields
}

WeightedPolicy is used to select clients randomly based on its loadFactor (weights)

Directories

Path Synopsis
internal
selector
Package selector implements weighted random selection algorithm
Package selector implements weighted random selection algorithm

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL