server

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package server accepts DNS queries from the network and answers them.

It owns the two listeners a resolver needs — a UDP socket and a TCP listener — the framing each requires, and the lifecycle around them. It does not resolve anything: queries go to a resolver.Handler the caller supplied, and what comes back is shaped to fit the transport it arrived on.

Sockets are handed in, not opened

Server.Serve takes a net.PacketConn and a net.Listener that the caller already opened. That is the primary entry point and Server.ListenAndServe is the convenience wrapper, not the other way round, for a reason that is not obvious from the signature: port 53 is privileged on every Unix, so a daemon either starts as root and drops privileges, receives an already-bound socket from systemd socket activation, or is granted CAP_NET_BIND_SERVICE. All three require the socket to be opened before and outside this package. A future refactor that "simplified" the API to own its own sockets would break privilege separation invisibly.

Backpressure, and why a goroutine per query is wrong

A DNS server reads datagrams from anyone who can send them. Handing each one to a fresh goroutine works beautifully until someone points a flood at it, and then the process holds a goroutine, a buffer and a half-finished upstream query per packet in flight, and dies of memory rather than of load.

So the number of queries in flight is bounded. Past the bound, UDP queries are DROPPED rather than queued: a datagram nobody is waiting for any more is worth nothing, and a queue only converts an overload into the same overload arriving later with worse latency. TCP is different — the connection is proof somebody is still there — so a TCP query waits for a slot instead.

Amplification

A UDP response is sent to whatever address the query claimed to come from, and that address may be forged. Every DNS server is therefore a potential amplifier pointed at a victim, and the size ratio is the weapon: a 40-octet query eliciting a 4000-octet answer multiplies an attacker's bandwidth a hundredfold.

This package cannot tell a forged source from a real one, so it limits what it can be made to emit. A UDP answer is never larger than the greater of 512 octets — the size RFC 1035 section 4.2.1 requires every implementation to accept — and the client's own advertised EDNS payload size, and never larger than MaxUDPResponse whatever the client claims. Anything longer is truncated with TC set, which makes a real client retry over TCP, and a spoofed source cannot. That is the whole defence, and it is why the size limit is enforced here rather than left to the caller.

The 512-octet floor is not a hole in it: an answer that small amplifies a minimal query about thirteenfold, against the thousandfold an uncapped one would, and refusing to honour it would leave a client advertising something tiny unable to receive anything at all.

Shutdown

Server.Shutdown stops accepting, lets queries already in flight finish, and returns when they have or when its context expires. A resolver that drops answers to queries it already accepted turns a routine restart into a visible outage on every device behind it.

Index

Constants

View Source
const (
	// MaxUDPResponse caps a UDP answer however large a payload the client
	// advertises. A client claiming it can receive 65535 octets is either
	// unusual or lying, and believing it is what turns this server into an
	// amplifier: the answer goes to whatever address the query claimed, and
	// that address may be forged. 1232 is the DNS Flag Day 2020 value, chosen
	// to avoid IPv6 fragmentation, and is what a real client can actually
	// receive.
	MaxUDPResponse = 1232

	// DefaultMaxInFlight bounds concurrent queries. It is a memory bound before
	// it is a throughput one: each in-flight query holds a buffer, a goroutine
	// and possibly an upstream connection.
	DefaultMaxInFlight = 2048

	// DefaultReadTimeout bounds how long a TCP connection may stay open with
	// nothing on it. RFC 7766 section 6.2.3 leaves it to the implementation;
	// short enough that a slowloris cannot hold connections indefinitely, long
	// enough that a client reusing a connection between queries is not punished.
	DefaultReadTimeout = 30 * time.Second

	// DefaultWriteTimeout bounds a blocked write, so one unresponsive peer
	// cannot pin a goroutine forever.
	DefaultWriteTimeout = 5 * time.Second

	// DefaultMaxTCPConns bounds simultaneous TCP connections.
	DefaultMaxTCPConns = 512

	// DefaultQueryTimeout bounds one query's resolution.
	DefaultQueryTimeout = 5 * time.Second
)

Limits that hold whatever the client asks for.

Variables

View Source
var (
	// ErrNoHandler reports a server constructed without anything to resolve
	// with.
	ErrNoHandler = errors.New("server: no handler configured")
	// ErrClosed reports use of a server that has been shut down.
	ErrClosed = errors.New("server: closed")
)

Errors this package returns.

Functions

This section is empty.

Types

type Options

type Options struct {
	// Handler resolves queries. Required.
	Handler resolver.Handler

	// MaxInFlight bounds concurrent queries. Zero selects
	// [DefaultMaxInFlight]; negative means unbounded, which is supported for
	// tests and is a bad idea in production.
	MaxInFlight int `json:"max_in_flight"`

	// MaxTCPConns bounds simultaneous TCP connections. Zero selects
	// [DefaultMaxTCPConns].
	MaxTCPConns int `json:"max_tcp_conns"`

	// ReadTimeout, WriteTimeout and QueryTimeout bound an idle connection, a
	// blocked write and one resolution. Zero selects the defaults above.
	ReadTimeout  time.Duration `json:"read_timeout"`
	WriteTimeout time.Duration `json:"write_timeout"`
	QueryTimeout time.Duration `json:"query_timeout"`

	// MaxUDPResponse caps UDP answers. Zero selects [MaxUDPResponse]; a larger
	// value is clamped to it, because the cap exists to bound amplification
	// rather than to express a preference.
	MaxUDPResponse int `json:"max_udp_response"`

	Clock   clock.Clock      `json:"-"`
	Metrics metrics.Recorder `json:"-"`
	Events  *events.Bus      `json:"-"`
	Logger  *slog.Logger     `json:"-"`
}

Options configure a Server.

func (Options) Validate

func (o Options) Validate() error

Validate reports every problem with o rather than the first.

type Server

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

Server answers DNS queries on a UDP socket, a TCP listener, or both.

A Server is used once: after Server.Shutdown it cannot be restarted, which keeps the lifecycle small enough to reason about. Build another.

func New

func New(opts Options) (*Server, error)

New returns a Server.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe(network, addr string) error

ListenAndServe opens the sockets itself and serves on them.

It is the convenience form. A daemon binding a privileged port should open the sockets before dropping privileges and call Server.Serve instead; see the package documentation.

func (*Server) Serve

func (s *Server) Serve(pc net.PacketConn, l net.Listener) error

Serve answers queries on pc and l until Server.Shutdown. Either may be nil.

It returns when both listeners have stopped, which for a healthy server means when Shutdown is called. The sockets are the caller's: Serve does not close them, because the caller may have received them from a service manager and may want them back.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops accepting and waits for queries already accepted to finish.

It returns ctx.Err() if they do not finish in time, having stopped accepting either way. Dropping answers to queries already accepted turns a routine restart into a visible outage on every device behind the resolver, which is why this waits rather than cancelling.

func (*Server) Stats

func (s *Server) Stats() Stats

Stats returns a snapshot.

type Stats

type Stats struct {
	// Queries accepted and answered.
	Queries  uint64 `json:"queries"`
	Answered uint64 `json:"answered"`
	// Dropped is UDP queries refused because the in-flight bound was reached.
	// A non-zero value under normal load means MaxInFlight is too low; under a
	// flood it means the bound is working.
	Dropped uint64 `json:"dropped"`
	// TCPRefused is connections turned away because MaxTCPConns was reached.
	// It is counted separately from Dropped because the remedy differs — one
	// points at MaxInFlight and the other at MaxTCPConns — and an operator
	// reading one number cannot tell which knob to turn.
	TCPRefused uint64 `json:"tcp_refused"`
	// Malformed is datagrams that could not be decoded at all.
	Malformed uint64 `json:"malformed"`
	// Truncated is UDP answers that did not fit and were cut with TC set.
	Truncated uint64 `json:"truncated"`
	// TCPConns accepted, and the number currently open.
	TCPConns uint64 `json:"tcp_conns"`
	TCPOpen  int64  `json:"tcp_open"`
	// RefusedLoop is messages discarded because they were themselves responses.
	// A non-zero value on a public resolver means somebody is probing for the
	// reflection loop that answering one would create.
	RefusedLoop uint64 `json:"refused_loop"`
	// Panics is handler panics contained. Any non-zero value is a bug
	// somewhere below this package, and the query it killed was answered
	// SERVFAIL rather than taking the process with it.
	Panics uint64 `json:"panics"`
	// WriteErrors is replies the socket refused after they were counted in
	// Answered.
	//
	// The two are related on purpose. Answered is incremented BEFORE the reply
	// is handed to the socket, so that a client holding a reply can never find
	// the server reporting it answered nothing — the counter may lead what was
	// delivered, and never lags it. WriteErrors is what makes the lead
	// accountable: delivered replies are Answered minus WriteErrors.
	WriteErrors uint64 `json:"write_errors"`
}

Stats reports what the server has done.

Jump to

Keyboard shortcuts

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