dns

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 2026 License: AGPL-3.0 Imports: 28 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 Allow added in v0.2.0

type Allow struct {
	Prefixes []netip.Prefix
	Keys     []zone.Name
}

Allow permits a transfer to a client inside one of the prefixes, or to one holding one of the keys. Both empty permits nobody, and nothing here spells everybody by accident.

func (Allow) MayTransfer added in v0.2.0

func (a Allow) MayTransfer(client netip.Addr, key, _ zone.Name) bool

MayTransfer implements Transfers. One entry matching is enough, which is what makes a key an entry rather than a second condition (docs/decisions/d28-tsig.md).

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

	// MaxTransfers is how many zone transfers may run at once, out of the
	// connections MaxTCPClients allows. A transfer arriving when they are all
	// in use is answered SERVFAIL and told to come back, rather than queued
	// behind one that may take minutes. Zero means [defaultTransfers]; a
	// negative value means no bound, which is a decision an operator makes on
	// purpose.
	MaxTransfers 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)

	// Transfers decides who may pull a whole zone off this server. Nil allows
	// nobody, which is what an unconfigured server does.
	Transfers Transfers

	// Keys are the TSIG keys this server verifies and signs with. Empty holds
	// none, and then a signed request is refused with BADKEY.
	Keys Keyring

	// Cookies are the secrets Server Cookies are computed under. Zero means
	// none have been minted yet, and until one is published the server answers
	// a query carrying a cookie without one of its own.
	Cookies CookieSecrets

	// History supplies the record changes between two serials, which is the one
	// thing an incremental transfer needs and no snapshot holds. Nil answers
	// every incremental request with a whole zone, which RFC 1995 §2 allows.
	History History

	// 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 CookieSecret added in v0.4.0

type CookieSecret [16]byte

CookieSecret is the key Server Cookies are computed under.

It is 128 bits because SipHash-2-4 takes a 128-bit key, and RFC 9018 §6 leaves no second choice of hash. The secret never leaves this server: a cookie is meaningless anywhere else, which is what makes rotating it a decision this server can take on its own.

type CookieSecrets added in v0.4.0

type CookieSecrets struct {
	// Current is what a cookie handed out now is computed under.
	Current CookieSecret
	// Previous is the secret before it, or zero on a server that has never
	// rotated.
	Previous CookieSecret
}

CookieSecrets are the secrets this server recognises its own cookies by.

Two, because a rotation cannot be instantaneous: a client holds a Server Cookie for up to an hour (RFC 9018 §4.3), and the secret those were built under is still worth checking against. Where they come from and when they change is the control plane's business, not this package's.

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

	// Refused says the query was turned away for want of a cookie while the
	// server was under load (D35), and what the client had brought with it.
	// [NotRefused] on every other exchange, which is nearly all of them.
	Refused Refusal

	// 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 History added in v0.2.0

type History interface {
	// Since returns the commits that took the zone from one serial to another,
	// oldest first, with the first starting at from and the last ending at to.
	//
	// The upper serial is the one the snapshot holds, not the newest the
	// journal has. The two differ for as long as a write takes to publish, and
	// a transfer that announced the newer one would have a secondary serve a
	// state this server does not.
	//
	// A range the history does not cover is false rather than an error: a full
	// transfer answers an incremental request correctly (RFC 1995 §2), so not
	// having the difference is a fallback, not a fault.
	Since(
		ctx context.Context, apex zone.Name, from, to zone.Serial,
	) ([]*journal.Commit, bool, error)
}

History is where an incremental transfer reads what a zone has done.

The query path may not reach the database (invariant 2), so this is the seam docs/decisions/ D26 leaves for the one thing a transfer needs that no snapshot holds: the record changes between two serials.

type Keyring added in v0.2.0

type Keyring map[zone.Name]TSIGKey

Keyring is the TSIG keys this server holds, by name.

Published rather than looked up. A signed query has to be verified before it is answered, and reading the database there would put a disk on the path of every query (invariant 2). It is copied out of the store as the server starts and again after every batch that creates or withdraws a key, the way the transfer list is (internal/publish).

A key that has been withdrawn is not in it: docs/decisions/d28-tsig.md clears its secret, so there would be nothing to verify against.

func (Keyring) Key added in v0.2.0

func (k Keyring) Key(name zone.Name) (TSIGKey, bool)

Key returns the key of that name, or false.

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 Notifier added in v0.2.0

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

Notifier tells secondaries that a zone has a new version.

It is the one part of this package that sends rather than answers. A notification is started by the control plane after a write and carries only what the published snapshot holds, so nothing here is on the path of a query (invariant 2).

func NewNotifier added in v0.2.0

func NewNotifier(cfg NotifyConfig) *Notifier

NewNotifier returns a notifier that has not opened its socket yet.

func (*Notifier) Close added in v0.2.0

func (n *Notifier) Close(ctx context.Context) error

Close stops sending and waits for the goroutines, or for ctx to give up.

func (*Notifier) Notify added in v0.2.0

func (n *Notifier) Notify(snap *Snapshot, apex zone.Name)

Notify starts telling the secondaries that a zone has a new version.

It is called with the snapshot queries are already being answered from, never with one about to be published: RFC 1996 §4.2 sends a notification once the version it announces is being served, and a secondary that came back early would be told to fetch a serial this server does not yet have.

A zone the snapshot does not hold has been deleted, and there is no version to announce. It returns without blocking; the sending happens elsewhere.

func (*Notifier) SetKeys added in v0.2.0

func (n *Notifier) SetKeys(ring Keyring)

SetKeys publishes the keys a target may name, for every notification from here on.

func (*Notifier) SetTargets added in v0.2.0

func (n *Notifier) SetTargets(targets []NotifyTarget)

SetTargets publishes who is told, for every notification from here on.

func (*Notifier) Start added in v0.2.0

func (n *Notifier) Start() error

Start opens the socket a notification goes out on and begins sending.

The socket is the notifier's own rather than the one queries arrive on: a response has to come back to the port the request left from, and that port is where every query in flight is being answered.

type NotifyConfig added in v0.2.0

type NotifyConfig struct {
	// Targets are the secondaries told when a zone changes. Empty tells
	// nobody, which is where a server starts (docs/decisions/d27-notify.md).
	Targets []NotifyTarget

	// Keys are the TSIG keys a target may name. One it does not hold sends the
	// notification unsigned and reports it: a secondary insisting on TSIG then
	// ignores the news rather than acting on something forgeable.
	Keys Keyring

	// OnError is called for a notification that could not be sent or that was
	// never answered. Never for a write: a notification that does not arrive
	// leaves the secondary on its refresh timer, which is where it was before
	// any of this existed. May be nil.
	OnError func(error)

	// Observe is called for each step of telling one secondary. It is what
	// feeds the metrics, composed by the wiring the way the query path's is.
	// May be nil, and then nothing is counted.
	Observe func(NotifyEvent)

	// Interval and Attempts override RFC 1996's recommendation. Zero takes it.
	// Tests set them; an operator has no reason to.
	Interval time.Duration
	Attempts int
}

NotifyConfig is what a Notifier needs. The zero value is usable.

type NotifyEvent added in v0.2.0

type NotifyEvent struct {
	Zone    zone.Name
	Target  netip.AddrPort
	Outcome NotifyOutcome
}

NotifyEvent is one step of telling one secondary about one zone.

type NotifyOutcome added in v0.2.0

type NotifyOutcome string

NotifyOutcome is what happened to one notification.

const (
	// NotifySent is one datagram written, retransmissions included.
	NotifySent NotifyOutcome = "sent"
	// NotifyAnswered is a secondary that responded, whatever it responded.
	NotifyAnswered NotifyOutcome = "answered"
	// NotifyAbandoned is a secondary that never did, and has now been left to
	// its own refresh timer.
	NotifyAbandoned NotifyOutcome = "abandoned"
)

type NotifyTarget added in v0.2.0

type NotifyTarget struct {
	Addr netip.AddrPort
	// Key is the TSIG key the notification is signed with, and is the zero
	// name for one that goes out unsigned (docs/decisions/d28-tsig.md).
	Key zone.Name
}

NotifyTarget is one secondary told when a zone changes.

type ProbeConfig added in v0.3.0

type ProbeConfig struct {
	// Targets are the secondaries asked. It is the notify list (D27): a
	// secondary worth telling is the one worth asking, and there is no second
	// list to keep in step with the first.
	Targets []NotifyTarget

	// Snapshots reports what this server publishes, which is what "in step"
	// is measured against. A probe compares the secondary with what is being
	// answered now, not with what it was told at the time.
	Snapshots Snapshots

	// OnError is called for a query that could not be sent or built. Never for
	// a secondary that is behind: that is what the probe is for, and it
	// travels as an event rather than as a fault.
	OnError func(error)

	// Observe is called for each answered question, and is what feeds the
	// metrics. May be nil, and then nothing is counted.
	Observe func(ProbeEvent)

	// Forget is called for a pair that has stopped existing, because a target
	// left the notify list or a zone was deleted. A gauge outlives what it
	// describes unless somebody says so. May be nil.
	Forget func(zone.Name, netip.AddrPort)

	// Wait and Floor override the constants above. Tests set them; an operator
	// has no reason to.
	Wait  time.Duration
	Floor time.Duration
}

ProbeConfig is what a Prober needs. The zero value is usable, and probes nobody: the notify list is where the targets come from and it starts empty.

type ProbeEvent added in v0.3.0

type ProbeEvent struct {
	Zone    zone.Name
	Target  netip.AddrPort
	Outcome ProbeOutcome

	// Lag is how far behind the secondary is, and is meaningful only for
	// [ProbeBehind]. D2 steps a serial once per commit, so it counts the
	// commits the secondary has yet to see rather than an opaque distance.
	Lag uint32
}

ProbeEvent is one answered question about one zone on one secondary.

type ProbeOutcome added in v0.3.0

type ProbeOutcome string

ProbeOutcome is what asking one secondary about one zone found.

const (
	// ProbeInStep is a secondary holding the serial this server publishes.
	ProbeInStep ProbeOutcome = "in_step"
	// ProbeBehind is one holding an older serial, which is the fault the
	// generated configuration cannot rule out (D34).
	ProbeBehind ProbeOutcome = "behind"
	// ProbeAhead is one holding a newer serial. D2 advances a zone by one per
	// commit here, so a secondary in front of us took its copy from somewhere
	// this server is not.
	ProbeAhead ProbeOutcome = "ahead"
	// ProbeUnordered is the pair RFC 1982 §3.2 leaves undefined, exactly half
	// the space apart. Neither is older, and saying so beats guessing.
	ProbeUnordered ProbeOutcome = "unordered"
	// ProbeSilent is a secondary that did not answer in time.
	ProbeSilent ProbeOutcome = "silent"
	// ProbeNoSerial is one that answered without a start of authority to read,
	// which is what an rcode other than NOERROR looks like from here.
	ProbeNoSerial ProbeOutcome = "no_serial"
)

type ProbeStanding added in v0.3.0

type ProbeStanding struct {
	Zone   zone.Name
	Target netip.AddrPort

	// Outcome is what the last finished question found, or the empty string
	// where the pair has not been asked yet.
	Outcome ProbeOutcome

	// Serial is what the secondary last said it holds, and Known whether it
	// ever said. A secondary that has gone quiet keeps the last serial it gave
	// while its outcome says the reading is old.
	Serial zone.Serial
	Known  bool

	// Lag is how many commits behind that serial is, where the last question
	// found the secondary behind.
	Lag uint32

	// At is when the last question finished.
	At time.Time
}

ProbeStanding is what is known about one zone on one secondary. It is a picture of the moment it was taken and is not held in step with anything.

type Prober added in v0.3.0

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

Prober asks the secondaries what serial they hold.

Like the notifier beside it, this is the part of the package that sends rather than answers, it is driven by the control plane, and nothing here is on the path of a query (invariant 2). See docs/decisions/, D36.

func NewProber added in v0.3.0

func NewProber(cfg ProbeConfig) *Prober

NewProber returns a prober that has not opened its socket yet.

func (*Prober) Close added in v0.3.0

func (p *Prober) Close(ctx context.Context) error

Close stops asking and waits for the goroutines to leave.

func (*Prober) Notified added in v0.3.0

func (p *Prober) Notified(ev NotifyEvent)

Notified takes a step of the notifier's work and schedules from it.

A notification going out puts the pair in flight, and the answer to it, or giving up on it, is what makes the pair worth asking about: the secondary has now had its chance. A zone nobody edits produces no probes this way at all, which is what keeps the traffic proportional to how much this server is changed rather than to how many zones it holds.

func (*Prober) SetTargets added in v0.3.0

func (p *Prober) SetTargets(targets []NotifyTarget)

SetTargets publishes who is asked, for every probe from here on.

func (*Prober) Standing added in v0.3.0

func (p *Prober) Standing() []ProbeStanding

Standing reports what is known about every pair, ordered by zone and then by secondary so that asking twice reads the same way twice.

func (*Prober) Start added in v0.3.0

func (p *Prober) Start() error

Start opens the socket probes go out on and begins asking.

The socket is the prober's own, for the reason the notifier's is: an answer comes back to the port the question left from, and that port is where every query in flight is being answered.

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 Refusal added in v0.4.0

type Refusal uint8

Refusal says why an exchange was refused for want of a cookie, and tells apart the two clients D35 treats differently: the one that can come back with the cookie it was just handed, and the one that implements no cookies at all and cannot.

The second is what D35 names as the case its decision leaves exposed, and counting it is where the measurement it asks for before response rate limiting is reopened would come from.

const (
	// NotRefused is every exchange this did not happen to.
	NotRefused Refusal = iota
	// RefusedCookieless is a query that carried no cookie option at all.
	// There is nothing to hand such a client back, so the refusal is a plain
	// REFUSED.
	RefusedCookieless
	// RefusedBadCookie is a query whose Server Cookie was not one of ours, or
	// had expired. It is answered BADCOOKIE with a fresh cookie in it, so one
	// retry is the whole cost.
	RefusedBadCookie
)

func (Refusal) String added in v0.4.0

func (r Refusal) String() string

String names the refusal for a metric label, and is empty for an exchange that was not refused.

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, from netip.Addr, tr Transport, out []byte,
) ([]byte, error)

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

from is the address the query arrived from. A Server Cookie is computed over it (RFC 9018 §4.4), so a cookie handed to one client is worthless to any other, and an address nobody can forge is the whole point of the exchange.

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) SetCookieSecrets added in v0.4.0

func (s *Server) SetCookieSecrets(secrets CookieSecrets)

SetCookieSecrets publishes the secrets Server Cookies are computed and checked under, for every query answered from now on.

The control plane mints and rotates them; this is where the query path finds out. A rotation reaches the next query rather than the next restart, which is what lets the previous secret be forgotten on a schedule instead of when somebody remembers to restart the server.

func (*Server) SetKeys added in v0.2.0

func (s *Server) SetKeys(ring Keyring)

SetKeys publishes the keys this server verifies and signs with, for every request from here on.

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) SetTransfers added in v0.2.0

func (s *Server) SetTransfers(t Transfers)

SetTransfers publishes who may pull a whole zone, for every transfer from here on. A nil one allows nobody.

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.

func (*Server) UnderLoad added in v0.4.0

func (s *Server) UnderLoad() bool

UnderLoad reports whether the kernel is dropping queries this server did not get to in time, which is what D38 settles as the meaning of load.

It is the state the last window left, so it is at most a window out of date and never a judgement made per query. A server that is not running is not under load.

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 Snapshots added in v0.3.0

type Snapshots interface {
	Snapshot() *Snapshot
}

Snapshots is where a probe reads the serial this server publishes. A *Server is one.

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 TSIGKey added in v0.2.0

type TSIGKey struct {
	Name      zone.Name
	Algorithm zone.TSIGAlgorithm
	Secret    []byte
}

TSIGKey is what signing and verifying need, and nothing else.

The query path may not reach the database (invariant 2), so the wiring resolves a key and hands over this much of it.

type Transfers added in v0.2.0

type Transfers interface {
	// MayTransfer decides one request. key is the name the request signed with
	// and verified against, or the zero name when it carried no signature.
	MayTransfer(client netip.Addr, key, apex zone.Name) bool
}

Transfers decides who may pull a whole zone off this server.

A nil one allows nobody, which is what an unconfigured server does: a zone is an inventory of a network, and a copy handed out cannot be taken back (docs/decisions/d26-outbound-zone-transfer.md).

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