dns

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

Documentation

Overview

Package dns is the data plane: it answers queries and does nothing else.

Everything here reads from an immutable Snapshot. Nothing here touches the database. The control plane builds a snapshot from stored records and swaps it in with a single atomic store, so a zone change never blocks a query that is already in flight, and a query holds one consistent view of the world for its whole lifetime without taking a lock. That is architecture invariant 2, and it is enforced mechanically: depguard refuses an import of internal/store from this package.

The store is the source of truth and the snapshot is a cache derived from it (invariant 8). A snapshot can always be rebuilt from the database; the database is never rebuilt from a snapshot. Crash recovery is therefore the same code path as startup, and there is nothing here to repair.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnanswerable = errors.New("dns: the query cannot be answered and must be dropped")

ErrUnanswerable reports a query that no response can be built for, so the only correct action is to drop it.

Functions

func ParseRcode

func ParseRcode(s string) (int, error)

ParseRcode is the inverse of RcodeName: it reads a response code written as a mnemonic, in any casing.

func RcodeName

func RcodeName(rcode int) string

RcodeName returns the mnemonic for a response code, or its number for one with no assigned name.

Response codes are twelve bits and only a handful are assigned, so a caller grouping by them (a metric label, a summary line) wants the name where there is one and something stable where there is not.

Types

type Answer

type Answer struct {
	// Rcode is the response code, numbered as the wire library numbers it.
	Rcode int

	// Authoritative is the AA bit. It says this server is an authority for the
	// name in the question (RFC 1035 §4.1.1): for that name, not for wherever
	// a CNAME chain happened to end.
	Authoritative bool

	// The three sections of a response. Their records belong to the snapshot
	// and are shared with every other query reading it: a caller may reorder or
	// drop them, and may never write to one.
	Answer     []wire.RR
	Authority  []wire.RR
	Additional []wire.RR

	// Extended explains a response that an RCODE alone would not (RFC 8914).
	Extended ExtendedError
}

Answer is a resolved question in the form of records: decided, not encoded. Keeping the two apart makes the search of RFC 1034 §4.3.2 a pure function of a snapshot and a question, testable without a socket.

The caller owns the value and is meant to reuse it. Answer.Reset empties the sections without releasing their memory, so a server holding one per worker allocates nothing per query (D12).

func (*Answer) Reset

func (a *Answer) Reset()

Reset empties the answer for reuse, keeping the memory its sections have already claimed.

type Config

type Config struct {
	// Addr is the address to listen on, as host:port. Empty means [defaultAddr].
	Addr string

	// Limits bound the messages the server sends. See [Limits].
	Limits Limits

	// UDPSockets is how many datagram sockets to open on Addr, each with a
	// reader of its own. Zero means one per available processor.
	UDPSockets int

	// TCPIdleTimeout is how long a connection may sit without a query before it
	// is closed. Zero means [defaultTCPIdle].
	TCPIdleTimeout time.Duration

	// MaxTCPClients is how many connections may be open at once. A connection
	// arriving when they all are is closed straight away rather than queued,
	// so that the accept loop never stalls behind a client that has stopped
	// talking. Zero means [defaultTCPClients]; a negative value means no bound
	// at all, which is a decision an operator makes on purpose.
	MaxTCPClients int

	// OnError is called for faults the server cannot act on itself: a socket
	// that fails to read, a connection that fails to write, a response that
	// fails to pack. Never for a merely malformed query, which is ordinary
	// traffic that an attacker could otherwise use to generate work.
	//
	// A hook rather than a logger: how the process logs is the wiring's
	// decision, not this package's. May be nil.
	OnError func(error)

	// Observe is called once per query, after the response has been written.
	// Both the metrics and the live query stream are fed from it; composing
	// them is the wiring's job.
	//
	// It runs on the goroutine that read the query and must not block: an
	// observer that waits stops a reader. Nil means nothing is watching, and
	// then not even the clock is read.
	Observe func(Event)
}

Config is what a Server needs to start. The zero value is usable and takes every default below.

type Event

type Event struct {
	// At is when the query was read, and Latency how long the exchange took
	// from that moment until the response had been written.
	At      time.Time
	Latency time.Duration

	// Client is where the query came from, and Transport how it arrived.
	Client    netip.AddrPort
	Transport Transport

	// Name, Type and Class are the question, as far as it could be read. Name
	// is empty when the message carried none; it is the name the client sent,
	// in the casing it sent, because that is what somebody watching the stream
	// is looking for.
	Name  string
	Type  zone.RRType
	Class zone.Class

	// Rcode is the response code sent, and Size the response in octets.
	Rcode int
	Size  int

	// Truncated is whether the response was cut to fit the transport and the
	// TC bit set (RFC 1035 §4.1.1).
	Truncated bool

	// Dropped is whether nothing was sent at all. Two messages have no safe
	// reply (architecture §2.2) and a response that cannot be packed
	// is a fault; either way the client waits for something that is not
	// coming, which is exactly what an operator is trying to find. Rcode and
	// Size mean nothing when it is set.
	Dropped bool
}

Event is one exchange, as everything watching the server sees it.

It carries what the query path already knows by the time the response is on the wire, and nothing that would have to be looked up: an observer is on the reader's goroutine, and work done there is work not spent answering the next query (architecture §2.9).

type ExtendedError

type ExtendedError struct {
	// Present tells an unset error apart from one carrying code 0, which
	// RFC 8914 §4.1 gives the meaning "Other" rather than "none".
	Present bool
	// Code is the INFO-CODE of RFC 8914 §2.
	Code uint16
	// Text is the EXTRA-TEXT: one operator-facing sentence, never a
	// machine-readable payload.
	Text string
}

ExtendedError is an extended DNS error (RFC 8914): a code and a short text saying why a response looks the way it does.

type Limits

type Limits struct {
	// MaxUDPResponse caps a UDP response however large a buffer the requestor
	// advertises. A requestor may claim to accept 4096 octets; whether the path
	// between us carries them is not something it can know.
	MaxUDPResponse uint16
}

Limits bound what the message layer will send.

func DefaultLimits

func DefaultLimits() Limits

DefaultLimits returns the limits a server runs with unless an operator says otherwise.

type Question

type Question struct {
	Name  zone.Name
	Class zone.Class
	Type  zone.RRType
}

Question is one question out of a query.

The name is already lowercased, since zone.Name holds every name that way (RFC 4343). Echoing the client's casing back is the message layer's job, so it never reaches the resolver.

type RecordSource

type RecordSource interface {
	IterZoneRecords(ctx context.Context, id zone.ZoneID) iter.Seq2[*zone.Record, error]
}

RecordSource yields the stored records of one zone, in any order. An interface rather than a store handle because the query path may not import the store (invariant 2); the method name matches store.Reader, so no adapter is needed.

type Responder

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

Responder turns the bytes of a query into the bytes of a response.

It holds the scratch one exchange needs (the parsed query, the response being assembled, the resolved answer) and reuses all of it, so a Responder belongs to a single worker goroutine and is **not** safe for concurrent use. A server keeps one per reader.

func NewResponder

func NewResponder(limits Limits) *Responder

NewResponder returns a responder bound to the given limits.

A zero MaxUDPResponse means "unset" and takes the default. One below the 512 octets of RFC 1035 §4.2.1 is raised to 512, because RFC 6891 §6.2.3 says a smaller advertised value is to be read as 512 and there is no reason for our own ceiling to behave differently.

func (*Responder) Observed

func (r *Responder) Observed() Event

Observed describes the exchange the responder has just finished.

func (*Responder) Respond

func (r *Responder) Respond(snap *Snapshot, query []byte, tr Transport, out []byte) ([]byte, error)

Respond answers query from snap and returns the response as bytes.

The response is packed into out when it fits; otherwise a new buffer is allocated, so a caller that wants neither should hand over a buffer of the largest message it is willing to send. The returned slice aliases out, and is only valid until the next call.

type Server

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

Server answers queries from a snapshot over UDP and TCP.

The snapshot sits in an atomic.Pointer and is the only coupling between the control and data planes (invariant 2). A commit publishes a new one with Server.SetSnapshot; a query in flight keeps the one it started with, so neither side ever waits for the other.

func NewServer

func NewServer(cfg Config) *Server

NewServer returns a server configured but not yet listening. It answers for nothing until a snapshot is published.

func (*Server) Addr

func (s *Server) Addr() net.Addr

Addr returns the address the server actually bound, which is what a configured port of 0 has to be read back from. It returns nil before Server.Start.

func (*Server) SetSnapshot

func (s *Server) SetSnapshot(snap *Snapshot)

SetSnapshot publishes a snapshot for every query from here on.

It is one atomic store and never blocks. Queries already running finish against the snapshot they started with, which is collected once the last of them lets go.

func (*Server) Shutdown

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

Shutdown stops the server and waits for the queries in flight, or for ctx to expire. Listeners close first so nothing new arrives, and open connections have their read deadline moved into the past, waking a reader that would otherwise wait out its idle period. A query being answered is answered.

func (*Server) Snapshot

func (s *Server) Snapshot() *Snapshot

Snapshot returns the snapshot queries are currently answered from.

func (*Server) Start

func (s *Server) Start() error

Start binds the sockets and begins answering, returning as soon as the listeners are up.

type Snapshot

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

Snapshot is every zone the server answers from, frozen.

The zero value and a nil *Snapshot are both an empty snapshot that answers for nothing, so a server can be wired up before its first build without a nil check on the query path.

func Build

func Build(ctx context.Context, zones []*zone.Zone, src RecordSource) (*Snapshot, error)

Build assembles a snapshot from the given zones and their stored records.

The build is sequential. At the record counts of D12 the cost is dominated by parsing record data, not by waiting on the store, and a parallel build is a contained change here if the cold-start budget ever demands it.

func Rebuild

func Rebuild(ctx context.Context, src Source) (*Snapshot, error)

Rebuild builds a snapshot from everything src holds.

It reads, patches nothing, and carries nothing over from an earlier snapshot (invariant 8), so startup and crash recovery are the same path and neither needs repair logic.

func (*Snapshot) Records

func (s *Snapshot) Records() int

Records returns the number of records the snapshot answers from, counting the SOA each zone carries.

func (*Snapshot) Resolve

func (s *Snapshot) Resolve(q Question, a *Answer)

Resolve answers q from the snapshot, writing the result into a.

This is the canonical name search of RFC 1034 §4.3.2: delegation before data, wildcards only where no closer name exists, NODATA where a name exists without the type asked for, NXDOMAIN only where the name exists nowhere. Nothing here reads the clock, the network or the database.

a is reset first, so the same one can be passed on every query. The records it points at belong to the snapshot and must not be modified.

func (*Snapshot) WithZone

func (s *Snapshot) WithZone(ctx context.Context, z *zone.Zone, src RecordSource) (*Snapshot, error)

WithZone returns a snapshot in which one zone has been rebuilt from the store, sharing every other zone with the receiver.

This is what a commit publishes. The new zone is built completely before the result is returned, so a caller that swaps it in exposes either the whole old state or the whole new one, never a half-built zone. A zone that has been disabled is removed rather than rebuilt.

func (*Snapshot) WithoutZone

func (s *Snapshot) WithoutZone(name zone.Name) *Snapshot

WithoutZone returns a snapshot with one zone gone, sharing the rest with the receiver. It returns the receiver unchanged when the zone was not there.

func (*Snapshot) Zones

func (s *Snapshot) Zones() int

Zones returns the number of zones the snapshot answers for.

type Source

type Source interface {
	RecordSource

	// IterZones streams every zone, in any order.
	IterZones(ctx context.Context) iter.Seq2[*zone.Zone, error]
}

Source is everything a snapshot is built from: the pair of streams a rebuild reads, which a store satisfies by being what it is. An interface rather than a store handle because the query path may not import the store (invariant 2).

type Transport

type Transport uint8

Transport is how a query reached the server.

It decides how large a response may grow and whether truncation applies at all: RFC 1035 §4.2.1 caps an unextended datagram at 512 octets, while §4.2.2 frames a stream message with a two-octet length instead.

const (
	// UDP is a datagram. A response too large for the requestor's buffer is
	// truncated and marked, so the client retries over TCP.
	UDP Transport = iota
	// TCP is a stream, where a response is not truncated for size.
	TCP
)

func (Transport) String

func (t Transport) String() string

String returns "udp" or "tcp", which is what a metric label and a stream entry both want to say.

Jump to

Keyboard shortcuts

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