middleware

package
v1.8.2 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 26 Imported by: 1

Documentation

Overview

Package middleware provides the DNS query middleware pipeline used by sdns. Middlewares are Constructors that produce Handlers; they register into a Registry, which Setup compiles into an immutable Pipeline. Each incoming DNS query runs through the pipeline via a Chain.

Index

Constants

View Source
const (
	// RecursionWorkEDEText is deliberately stable because it is returned to
	// clients as RFC 8914 Extended DNS Error text in enforce mode.
	RecursionWorkEDEText = "Recursion work budget exceeded"

	// RecursionWorkEDECode deliberately uses RFC 8914's finalized catch-all
	// code. The more specific "Unable to Conform to Policy" registry entry
	// still references an Internet-Draft, so it is not part of this wire
	// contract.
	RecursionWorkEDECode = dns.ExtendedErrorCodeOther

	// DNSSECWorkEDEText and DNSSECWorkEDECode distinguish validation-work
	// exhaustion from network fan-out on the wire.
	DNSSECWorkEDEText = "DNSSEC validation work budget exceeded"
	DNSSECWorkEDECode = dns.ExtendedErrorCodeDNSSECIndeterminate
)
View Source
const SidecarChainCap = 10

SidecarChainCap bounds a chase composition's segments; it mirrors the wire chase depth (the cache asserts its own bound fits at compile time).

Variables

View Source
var (
	// ErrResolutionAttemptLimit identifies a request-tree retry-guard
	// rejection. It is request-local: callers must not treat it as evidence
	// that the upstream server is unhealthy.
	ErrResolutionAttemptLimit = errors.New("resolution attempt limit exceeded")

	// ErrFailureProbeLimit identifies a follower shed after the bounded
	// re-election of an expired RFC 9520 failure probe. The limit belongs to
	// one request cohort and must never create shared failure-cache state.
	ErrFailureProbeLimit = errors.New("failure probe retry limit exceeded")
)
View Source
var DefaultRegistry = NewRegistry()

DefaultRegistry is the package-level registry used by the top-level Register / RegisterAt / RegisterBefore wrappers. Middleware packages register into it from their init hooks.

View Source
var ErrMaxRecursion = errors.New("queryer: max recursion depth exceeded")

ErrMaxRecursion signals that a Queryer.Query call nested past the recursion bound. Built-in paths (CNAME/DNAME/resolver depth) have their own caps; this is the generic safety net for plugin middleware that dispatches the queryer from inside its own ServeDNS and could otherwise loop forever.

View Source
var ErrNoResponse = errors.New("queryer: no response written")

ErrNoResponse signals that the sub-pipeline ran without any middleware writing a response.

View Source
var ErrRecursionWorkLimit = errors.New("recursion work limit exceeded")

ErrRecursionWorkLimit is the sentinel carried by a RecursionWorkLimitError.

View Source
var ErrWireFallback = errors.New("middleware: wire path unavailable, use WriteMsg")

ErrWireFallback tells a wire-serving caller to retake the ordinary *dns.Msg path. It is a routing signal, not a failure: an implementation MUST return it before any bytes reach the transport, so the caller can re-serve the same response without a double write.

Functions

func BeginResolutionAttempt added in v1.7.4

func BeginResolutionAttempt(ctx context.Context, q dns.Question, endpoint, transport string) error

BeginResolutionAttempt records an attempt in the current request tree. A missing guard is tolerated for low-level callers that intentionally operate outside a middleware request.

func BeginResolutionAttemptCanonical added in v1.8.0

func BeginResolutionAttemptCanonical(
	ctx context.Context,
	q dns.Question,
	endpoint, transport string,
) error

BeginResolutionAttemptCanonical is BeginResolutionAttempt for a caller whose endpoint is already canonically spelled. See BeginCanonical for what that promise means.

func CanonicalResolutionEndpoint added in v1.7.4

func CanonicalResolutionEndpoint(endpoint string) string

CanonicalResolutionEndpoint normalizes endpoint spellings before they are used as attempt-guard or duplicate-suppression identities.

func CheckRecursionWorkLocalLimit added in v1.7.4

func CheckRecursionWorkLocalLimit(ctx context.Context, kind RecursionWorkKind, used uint32) error

CheckRecursionWorkLocalLimit checks a local DNSSEC work limit in the current request tree. used is the number of candidates or signatures already examined, so the first rejected value in enforce mode is exactly the limit.

func ClearWireAD added in v1.8.0

func ClearWireAD(body []byte)

ClearWireAD clears the AD bit in a packed message header in place.

func DebitRecursionWork added in v1.7.4

func DebitRecursionWork(ctx context.Context, kind RecursionWorkKind) error

DebitRecursionWork debits the current tree when accounting is active.

func FinishRecursionWork added in v1.7.4

func FinishRecursionWork(ctx context.Context)

FinishRecursionWork publishes the current tree exactly once. Normal client pipelines finish at the outer Chain boundary; direct Resolver API callers use this when they had to establish their own standalone ledger.

func HasClientECS added in v1.7.4

func HasClientECS(ctx context.Context) bool

HasClientECS reports whether the original client query carried EDNS Client Subnet, even if an earlier middleware stripped the option from the message.

func IsBestEffortRecursionWork added in v1.7.4

func IsBestEffortRecursionWork(ctx context.Context) bool

IsBestEffortRecursionWork reports whether work debited through ctx belongs to an optional branch.

func IsInternal added in v1.6.4

func IsInternal(ctx context.Context) bool

IsInternal reports whether ctx was tagged by MarkInternal. Intended for plugin middleware that wants a ctx-based internal signal; sdns's own middlewares read the writer flag instead.

func IsRequestLocalResolutionError added in v1.7.4

func IsRequestLocalResolutionError(err error) bool

IsRequestLocalResolutionError reports whether err describes this request tree rather than shared upstream resolution state. Such failures must never be admitted to the RFC 9520 failure cache.

func List

func List() []string

List returns every registered middleware name in insertion order, including disabled ones. Before Setup it returns names from the DefaultRegistry; after Setup it returns the snapshot captured at Build.

func MarkClientECS added in v1.7.4

func MarkClientECS(ctx context.Context) context.Context

MarkClientECS returns a context that records that the original client query carried EDNS Client Subnet. It is idempotent so nested pipelines can preserve the marker without adding another context node. The marker is a scalar and request-lifetime, so it pins to the carrier when one exists; a foreign context gets a value node as before.

func MarkInternal added in v1.6.4

func MarkInternal(ctx context.Context) context.Context

MarkInternal returns a derived ctx tagged as originating from an internal sub-pipeline run. Provided as public API for plugin middleware that wants to signal internal-ness without relying on the BufferWriter's Internal() flag (e.g. when a plugin spawns its own internal work without going through Queryer.Query).

sdns's own Queryer.Query does NOT call MarkInternal — the BufferWriter it installs already reports Internal()==true, and every in-tree consumer (cache.ServeDNS dedup guard, cache-hit rate limiter) reads the writer flag. Skipping MarkInternal on the hot path saves one context.valueCtx allocation per internal sub-query.

func MarkRequestLocalFailureResponse added in v1.7.4

func MarkRequestLocalFailureResponse(ctx context.Context, msg *dns.Msg, err error)

MarkRequestLocalFailureResponse attaches exact request-local provenance to a terminal DNS response. Pointer identity prevents an unrelated upstream SERVFAIL later selected by failover from inheriting a losing branch's local attempt rejection or deadline.

func MarkValidatedDenialResponse added in v1.7.4

func MarkValidatedDenialResponse(ctx context.Context, msg *dns.Msg, denial ValidatedDenial)

MarkValidatedDenialResponse attaches explicit, resolver-local validation provenance to an exact NXDOMAIN response. It intentionally does not inspect or trust the response's AD bit.

func MarkValidatedNegativeProofResponse added in v1.7.4

func MarkValidatedNegativeProofResponse(
	ctx context.Context,
	msg *dns.Msg,
	negative ValidatedNegativeProof,
)

MarkValidatedNegativeProofResponse attaches explicit resolver-local validation provenance to an exact NXDOMAIN or terminal NODATA response. The caller-supplied Proof pointer is intentionally ignored: a fresh mark always originates at msg, and only explicit propagation may retain an older terminal proof identity.

func PropagateValidatedDenialResponse added in v1.7.4

func PropagateValidatedDenialResponse(ctx context.Context, from, to *dns.Msg) bool

PropagateValidatedDenialResponse copies exact-response provenance from one NXDOMAIN response identity to another. This is the only supported way for a middleware that replaces a response object to preserve validated denial metadata; ordinary dns.Msg copying does not do so.

func PropagateValidatedNegativeProofResponse added in v1.7.4

func PropagateValidatedNegativeProofResponse(ctx context.Context, from, to *dns.Msg) bool

PropagateValidatedNegativeProofResponse preserves the exact terminal proof when a middleware builds a replacement negative response (for example an outer CNAME/DNAME response). Ordinary dns.Msg copying is deliberately not a trust-propagation operation.

func Ready added in v1.1.0

func Ready() bool

Ready reports whether Setup has completed.

func RecursionWorkEDE added in v1.7.4

func RecursionWorkEDE(ctx context.Context) (uint16, string)

RecursionWorkEDE returns the request tree's latched enforcement EDE. When no local rejection is latched (for example, an upstream returned a generic policy error), it preserves the existing recursion-work fallback.

func RecursionWorkEnforcementError added in v1.7.4

func RecursionWorkEnforcementError(ctx context.Context) error

RecursionWorkEnforcementError returns the local request tree's first policy rejection. Checking this context-owned state, rather than trusting an EDE received on the wire, preserves shadow-mode compatibility with upstreams that legitimately emit the same policy code.

func RecursionWorkErrorEDE added in v1.7.4

func RecursionWorkErrorEDE(ctx context.Context, err error) (uint16, string)

RecursionWorkErrorEDE maps a directly returned recursion-work error before consulting the request tree's latched rejection. Direct Queryer and Exchange errors may carry more specific DNSSEC provenance than the context fallback.

func RecursionWorkRootOwned added in v1.7.4

func RecursionWorkRootOwned(ctx context.Context) bool

RecursionWorkRootOwned reports whether an outer middleware Chain owns the ledger's completion boundary, including when the ledger has not yet been materialized.

func Register

func Register(name string, c Constructor)

Register is a package-level shortcut for DefaultRegistry.Register.

func RegisterAt added in v1.1.0

func RegisterAt(name string, c Constructor, idx int)

RegisterAt is a package-level shortcut for DefaultRegistry.RegisterAt.

func RegisterBefore added in v1.1.0

func RegisterBefore(name string, c Constructor, before string)

RegisterBefore is a package-level shortcut for DefaultRegistry.RegisterBefore.

func RejectRecursionWork added in v1.7.4

func RejectRecursionWork(ctx context.Context, kind RecursionWorkKind) error

RejectRecursionWork records a governor rejection that occurs outside an accepted-work counter. Shadow mode observes it without replacing the original operational error; enforce mode returns a typed policy error.

func RequestLocalFailureForResponse added in v1.7.4

func RequestLocalFailureForResponse(ctx context.Context, msg *dns.Msg) error

RequestLocalFailureForResponse returns the typed request-local error attached to this exact response, if any.

func Reset added in v1.6.3

func Reset()

Reset clears the global Pipeline and DefaultRegistry state. It is intended for tests that need a clean slate between runs; production code should never call it.

func ResponseSize added in v1.8.0

func ResponseSize(w ResponseWriter) int

ResponseSize returns the response's length on the wire. Observers ask for it instead of measuring a parsed message: on the byte path that measurement would force an unpack, and a message decoded from bytes reports an inflated uncompressed length because Unpack does not restore Compress. A writer that cannot answer falls back to that measurement.

func Setup

func Setup(cfg *config.Config)

Setup builds the DefaultRegistry against cfg, loads external plugins, publishes the resulting Pipeline globally, and auto-wires sub-pipeline Queryers and shared Stores into every handler that implements the corresponding *Setter interface. It panics if called more than once without an intervening Reset.

Wiring sequence (after Build):

  1. Build queryerSub by filtering handlers that report ClientOnly()==true.
  2. Build prefetchSub as queryerSub without the cache handler (named "cache") — prefetch must reach the upstream resolver / forwarder instead of returning its own about-to-expire entry.
  3. Construct a PipelineQueryer for each sub-pipeline.
  4. Walk enabled handlers and call SetQueryer / SetPrefetchQueryer / SetStore on anything that implements them, sourcing the Store from whichever handler implements StoreProvider.

This keeps the main package free of wiring logic — every middleware declares its participation in the internal chain (ClientOnly), and every consumer declares what it needs (QueryerSetter, StoreSetter, etc.).

func WithBestEffortRecursionWork added in v1.7.4

func WithBestEffortRecursionWork(ctx context.Context) context.Context

WithBestEffortRecursionWork marks optional work that must stop at the shared cap without turning a successful required branch into a policy failure.

func WithRecursionWork added in v1.7.4

func WithRecursionWork(ctx context.Context, ledger *RecursionWorkLedger) context.Context

WithRecursionWork returns a derived context carrying the exact ledger.

func WithResolutionAttemptGuard added in v1.7.4

func WithResolutionAttemptGuard(ctx context.Context, guard *ResolutionAttemptGuard) context.Context

WithResolutionAttemptGuard anchors guard to ctx. Anchoring is required for detached work because its originating ResponseMeta can be reset and reused. The request tree's first anchor lands in the deadline carrier's request-lifetime pin — every internal sub-query then finds it without deriving a context. Anchoring over any existing, different anchor always derives an ordinary value node instead: the pin lives on the carrier the whole tree shares, so pinning an override would hand the new guard to the base and sibling contexts while the target subtree kept its nearer anchor — the exact opposite of the caller's intent.

func WithResponseMeta added in v1.7.3

func WithResponseMeta(ctx context.Context, m *ResponseMeta) context.Context

WithResponseMeta returns a derived ctx carrying m as the request tree's response metadata sink.

Types

type BufferWriter added in v1.6.4

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

BufferWriter is the Transport used inside Queryer.Query. It captures the response in memory and presents itself as a TCP connection so edns.ServeDNS picks the TCP-sized (MaxMsgSize) EDNS buffer rather than truncating internal replies at 512 bytes.

Internal() always reports true. middleware.responseWriter.Reset propagates that through the interface check so the cache middleware's remaining Internal() branches keep behaving correctly during Phase 3 — those branches are replaced with queryer.IsInternal(ctx) in Phase 4.

func (*BufferWriter) Close added in v1.6.4

func (w *BufferWriter) Close() error

Close satisfies Transport.

func (*BufferWriter) Internal added in v1.6.4

func (w *BufferWriter) Internal() bool

Internal reports this writer as belonging to an internal sub-pipeline run. middleware.responseWriter.Reset propagates it via the interface check.

func (*BufferWriter) LocalAddr added in v1.6.4

func (w *BufferWriter) LocalAddr() net.Addr

LocalAddr satisfies Transport.

func (*BufferWriter) Msg added in v1.6.4

func (w *BufferWriter) Msg() *dns.Msg

Msg returns the captured response (nil if the sub-pipeline never wrote).

func (*BufferWriter) Proto added in v1.6.4

func (w *BufferWriter) Proto() string

Proto is consulted by edns.ServeDNS to decide the UDP vs TCP EDNS buffer cap. Returning "tcp" keeps internal replies from being truncated at 512 bytes.

func (*BufferWriter) RemoteAddr added in v1.6.4

func (w *BufferWriter) RemoteAddr() net.Addr

RemoteAddr satisfies Transport.

func (*BufferWriter) Write added in v1.6.4

func (w *BufferWriter) Write(b []byte) (int, error)

Write unpacks b and captures the parsed message.

func (*BufferWriter) WriteMsg added in v1.6.4

func (w *BufferWriter) WriteMsg(m *dns.Msg) error

WriteMsg captures m as the recorded response.

func (*BufferWriter) Written added in v1.6.4

func (w *BufferWriter) Written() bool

Written reports whether any middleware in the sub-pipeline wrote a response.

type Chain added in v1.1.0

type Chain struct {
	Writer  ResponseWriter
	Request *Request

	// Meta is the pooled backing storage for the request's
	// ResponseMeta. The first middleware that needs a meta sink and
	// finds none in ctx establishes &Meta via WithResponseMeta;
	// nested pipelines then reuse the ctx pointer rather than their
	// own chain's field.
	Meta ResponseMeta
	// contains filtered or unexported fields
}

Chain carries per-request state through the middleware pipeline. Instances are reused via a sync.Pool: NewChain allocates the fixed pipeline reference, Reset rebinds the per-request writer + request.

func NewChain added in v1.1.0

func NewChain(handlers []Handler) *Chain

NewChain returns a Chain bound to the given handler pipeline. The slice is captured by reference and must not be mutated by the caller after this call.

func (*Chain) AllowDirectPack added in v1.8.0

func (ch *Chain) AllowDirectPack()

AllowDirectPack declares that the transport beneath this chain's writer is an SDNS-owned UDP, TCP or DoT sink whose Write sends raw wire bytes unchanged, so WriteMsg may pack in pooled storage and skip the library's per-message allocations.

It is a declaration with one intended caller — the server's owned-listener ingress, immediately after Reset — never an inference from address types or proto strings, which plugin writers can share without sharing the byte-sink property. Reset clears it, so a pooled chain cannot carry the capability to the next request. A chain whose writer was replaced by a custom ResponseWriter implementation is left alone.

func (*Chain) Bind added in v1.8.0

func (ch *Chain) Bind(handlers []Handler, workPolicy RecursionWorkPolicy)

Bind prepares caller-owned storage as a chain over handlers. It is how a transport gives its job slab a chain of its own: the strict path must not draw one from a pool, whose contents the collector empties on the second GC — precisely the state the hard-zero measurement fences for.

It is idempotent and cheap enough to call per request; the pipeline's handler list is immutable, so re-binding only restates it.

func (*Chain) Cancel added in v1.1.0

func (ch *Chain) Cancel()

Cancel stops the chain without writing a response. Subsequent Next calls become no-ops.

func (*Chain) CancelWithRcode added in v1.1.7

func (ch *Chain) CancelWithRcode(rcode int, do bool)

CancelWithRcode writes a reply with the given rcode and stops the chain. do controls the DO bit in the response's OPT record.

func (*Chain) Finish added in v1.8.0

func (ch *Chain) Finish()

Finish closes a served request's remaining lifecycle: the detached context and the recursion-work boundary a mid-chain materialization established. A pooled chain gets this through PutChain; a job-owned one is finished by its transport.

func (*Chain) Handoff added in v1.8.0

func (ch *Chain) Handoff() bool

Handoff reports whether a handler declined blocking work this serve.

func (*Chain) InlineOnly added in v1.8.0

func (ch *Chain) InlineOnly() bool

InlineOnly reports whether the current serve refuses to block.

func (*Chain) MarkHandoff added in v1.8.0

func (ch *Chain) MarkHandoff()

MarkHandoff records that the serve stopped short of blocking work and the query needs a full pass on a worker.

func (*Chain) Materialize added in v1.8.0

func (ch *Chain) Materialize(ctx context.Context) (context.Context, *dns.Msg)

Materialize returns the decoded request, decoding a wire-born request on first call. That first decode is the one-way transition off the strict path: the remaining handlers must run on the returned context — a fresh lazy deadline parented on a stable context (never the recycled job carrier), carrying the ECS marker, the same ResponseMeta, and a fresh recursion-work completion boundary whose lifecycle the chain closes when the serve completes. A nil message means the packet failed decoding; the chain is canceled and the caller must stop without calling Next.

func (*Chain) Next added in v1.1.0

func (ch *Chain) Next(ctx context.Context)

Next invokes the next handler in the chain. Each handler is responsible for calling Next to continue, or Cancel/CancelWithRcode to stop.

func (*Chain) Replay added in v1.8.0

func (ch *Chain) Replay() bool

Replay reports whether this serve is the worker pass after an inline handoff.

func (*Chain) Reset added in v1.1.0

func (ch *Chain) Reset(w Transport, r *dns.Msg)

Reset rebinds the chain to a fresh writer + decoded request for pool reuse. The internal sub-pipeline, DoH/DoQ and embedders enter here; the server's raw ingress enters through ResetWire.

func (*Chain) ResetWire added in v1.8.0

func (ch *Chain) ResetWire(w Transport, r *Request)

ResetWire rebinds the chain to a wire-born request living in transport job storage. No decoded message exists until a handler materializes one.

func (*Chain) SetInlineOnly added in v1.8.0

func (ch *Chain) SetInlineOnly()

SetInlineOnly declares that this serve runs on a transport reader that must not block: a handler that would start an upstream resolution marks the chain for handoff instead and returns unwritten. The transport then replays the query on a worker.

func (*Chain) SetReplay added in v1.8.0

func (ch *Chain) SetReplay()

SetReplay declares that this serve finishes a query an inline pass handed off: the full pipeline ran once on the transport reader up to the handoff point, so every entry effect — a limiter token, a scored query, a tap's query frame, a per-query statistic — has already happened. A handler with such an effect checks Replay and skips it; its response-side observers (writer wrappers) install as always, because this pass is the one that writes.

type ClientOnly added in v1.6.4

type ClientOnly interface {
	ClientOnly() bool
}

ClientOnly marks a Handler as serving real client traffic only. Middlewares that implement this method returning true are excluded from the internal sub-pipeline built in Setup — they exist to observe, rate-limit, or authorise external client queries, and either add noise (metrics, dnstap, accesslog) or actively hurt (ratelimit, accesslist, reflex) when an internal sub-query traverses them.

The default for handlers that do NOT implement ClientOnly is "include in the internal sub-pipeline" — safe for anything participating in query resolution (hostsfile, blocklist, cache, failover, resolver, forwarder, etc).

type Constructor added in v1.6.3

type Constructor func(*config.Config) Handler

Constructor builds a Handler from config. A Constructor that returns a typed-nil pointer (e.g. `(*Reflex)(nil)`) signals that the middleware is disabled for this config and is skipped at Build time.

type ContextStore added in v1.7.4

type ContextStore interface {
	Store
	GetWithContext(ctx context.Context, req *dns.Msg) (*dns.Msg, bool)
}

ContextStore is the optional request-tree-aware cache contract. The built-in cache uses ctx to share bounded NSEC3 hash results with required DNSSEC validation and to retain client CD/ECS bypass policy across resolver-private DS and DNSKEY sub-queries. Third-party stores can keep implementing Store.

type CutStore added in v1.7.3

type CutStore interface {
	Store
	SetFromResponseWithCut(resp *dns.Msg, keyCD bool, cutUntil time.Time, cutKey uint64)
}

CutStore is the extended production cache contract. Keeping Store's Phase 1b signature stable avoids forcing plugin/test stores to implement lineage identity before Phase 3 needs it; the built-in cache implements both.

type DNSSECCryptoLimiter added in v1.7.4

type DNSSECCryptoLimiter interface {
	TryAcquire() (release func(), ok bool)
}

DNSSECCryptoLimiter is the narrow shared concurrency seam required by optional cache-side DNSSEC work. TryAcquire is deliberately non-blocking: RFC 8198 synthesis is an optimization, so saturation must fall through to ordinary resolution instead of waiting ahead of required validation.

type DNSSECCryptoLimiterProvider added in v1.7.4

type DNSSECCryptoLimiterProvider interface {
	DNSSECCryptoLimiter() DNSSECCryptoLimiter
}

DNSSECCryptoLimiterProvider is implemented by the resolver that owns the process-wide DNSSEC concurrency gate.

type DNSSECCryptoLimiterSetter added in v1.7.4

type DNSSECCryptoLimiterSetter interface {
	SetDNSSECCryptoLimiter(DNSSECCryptoLimiter)
}

DNSSECCryptoLimiterSetter is implemented by optional DNSSEC work consumers such as the RFC 8198 proof cache. Setup wires the resolver-owned instance before publishing the pipeline.

type Handler added in v1.1.0

type Handler interface {
	// Name returns the middleware name. It must match the name used in
	// Register so Pipeline.Get can resolve the handler back.
	Name() string

	// ServeDNS processes a DNS query. A handler is expected to call
	// ch.Next to continue the chain, or ch.Cancel / ch.CancelWithRcode
	// to stop it.
	ServeDNS(ctx context.Context, ch *Chain)
}

Handler is the middleware interface. Implementations must be safe for concurrent use.

func Get

func Get(name string) Handler

Get returns an enabled handler by name from the global Pipeline. Returns nil before Setup or if the middleware is disabled / unknown.

func Handlers

func Handlers() []Handler

Handlers returns the enabled handlers from the global Pipeline. Returns nil before Setup.

type HandlerFunc added in v1.5.0

type HandlerFunc func(context.Context, *Chain)

HandlerFunc adapts a plain function into a Handler. Handy in tests that want to inject a one-off behaviour without declaring a new type.

func (HandlerFunc) Name added in v1.5.0

func (f HandlerFunc) Name() string

Name returns the fixed label "HandlerFunc".

func (HandlerFunc) ServeDNS added in v1.5.0

func (f HandlerFunc) ServeDNS(ctx context.Context, ch *Chain)

ServeDNS dispatches to f.

type InlineBarrier added in v1.8.0

type InlineBarrier interface {
	InlineBarrier() bool
}

InlineBarrier marks the Handler that makes a pipeline safe to run on a transport reader: it honors Chain.InlineOnly by declining blocking work — an upstream resolution, a queue wait — with MarkHandoff instead of running it. The cache is the barrier in the standard pipeline. The server enables the inline fast path only when some handler declares this; a pipeline without a barrier would carry a reader all the way into the resolver, and a reader that blocks for a resolution stalls every packet behind it on that socket.

Handlers whose ServeDNS entry has side effects that must happen exactly once per client query — a rate-limit token, a scored query, a tap's query frame, a per-query statistic — check Chain.Replay and skip the effect on the worker pass that finishes a handed-off query; their response-side writer wrappers install on both passes, because only the pass that writes will trip them. Handlers keying purely off the response — metrics and accesslog gate on Written() — need no check.

type Pipeline added in v1.6.3

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

Pipeline is the compiled, immutable middleware chain produced by Registry.Build. Handler fields are set at construction time and never mutated, so every read is safe without synchronization. chainPool is internally mutable (sync.Pool's contract) but serves the same Pipeline across all callers — pooling *Chain keeps per-internal-query allocations off the hot path for Queryer.

func GlobalPipeline added in v1.6.4

func GlobalPipeline() *Pipeline

GlobalPipeline returns the active pipeline snapshot, or nil before Setup. Startup wiring (queryer construction, api purge hooks) reads this once to derive sub-pipelines and enumerate purgers.

func (*Pipeline) BindChain added in v1.8.0

func (p *Pipeline) BindChain(ch *Chain)

BindChain prepares caller-owned storage as a chain over this pipeline's handlers, so a transport can keep the chain in its own job slab instead of drawing one from the pool. The strict path uses this; everything else keeps NewChain/PutChain.

func (*Pipeline) Get added in v1.6.3

func (p *Pipeline) Get(name string) Handler

Get returns the enabled handler with the given name or nil if the middleware is not registered or is disabled for the current config.

func (*Pipeline) Handlers added in v1.6.3

func (p *Pipeline) Handlers() []Handler

Handlers returns the enabled handlers in chain order. The returned slice aliases Pipeline's internal storage; callers must not mutate it.

func (*Pipeline) List added in v1.6.3

func (p *Pipeline) List() []string

List returns every registered middleware name in order, including disabled ones. Useful for diagnostics.

func (*Pipeline) NewChain added in v1.6.4

func (p *Pipeline) NewChain() *Chain

NewChain returns a Chain bound to this pipeline's handlers, pulled from the pipeline's own sync.Pool. Callers that dispatch internal sub-queries (queryer.Queryer) should return the chain via PutChain after use to keep per-sub-query allocations off the hot path. Callers that build a chain for long-lived use don't need to return it.

func (*Pipeline) Purgers added in v1.6.4

func (p *Pipeline) Purgers() []Purger

Purgers returns every enabled handler that implements Purger, in pipeline order. The api purge endpoint iterates this to invalidate both the cache middleware's entries and the resolver handler's nameserver cache.

func (*Pipeline) PutChain added in v1.6.4

func (p *Pipeline) PutChain(ch *Chain)

PutChain returns ch to the pipeline's pool. Safe to call with a chain from any pipeline — the sync.Pool is per-pipeline so cross put/get only causes a pool mismatch (never a correctness bug), but callers should pair PutChain with NewChain from the same Pipeline to keep the pool warm.

func (*Pipeline) SubPipeline added in v1.6.4

func (p *Pipeline) SubPipeline(skip ...string) *Pipeline

SubPipeline returns a new Pipeline containing the same handlers in the same order, minus any whose Name() is listed in skip. Used to build the internal sub-pipeline for queryer.Queryer: client-only guards (metrics, dnstap, accesslist, ratelimit, reflex, accesslog, loop) are dropped so internal sub-queries don't pollute observability or double-count against rate limits, but local-answer middlewares (hostsfile, blocklist, kubernetes, as112), cache, failover, and resolver/forwarder stay.

The returned Pipeline is independent of the receiver — it has its own byName index and handler slice. The names list carries forward the full registered list for diagnostics.

type PrefetchQueryerSetter added in v1.6.4

type PrefetchQueryerSetter interface {
	SetPrefetchQueryer(q Queryer)
}

PrefetchQueryerSetter is implemented by handlers that consume the prefetch sub-pipeline Queryer (today: cache middleware's prefetch worker).

type Purger added in v1.6.4

type Purger interface {
	Purge(q dns.Question)
}

Purger is implemented by handlers that maintain cacheable state which can be invalidated by question. The cache middleware and the resolver handler both implement it — Cache purges its positive / negative entries, the resolver purges its nameserver cache.

The api purge endpoint iterates every Purger in the pipeline instead of synthesising a CHAOS-NULL query and routing it through ServeDNS; that keeps purge as a side-effect operation rather than a pseudo-DNS flow that has to survive every middleware on the way through.

type QueryPolicyGate added in v1.8.2

type QueryPolicyGate interface {
	QueryWireHitGate() WireHitGate
}

QueryPolicyGate is implemented by a policy middleware's response writer to carry this query's own gate — the channel through which query-time state (held candidates, a decision that already fell, an exemption) reaches the byte-serve judgment. When the writer offers one, the cache consults it instead of the globally wired gate, judge and count alike, for the whole hit. A nil return falls back to the global gate.

type Queryer added in v1.6.4

type Queryer interface {
	Query(ctx context.Context, req *dns.Msg) (*dns.Msg, error)
}

Queryer answers a client-shaped DNS query through the internal sub-pipeline.

func NewPipelineQueryer added in v1.6.4

func NewPipelineQueryer(sub *Pipeline) Queryer

NewPipelineQueryer returns a Queryer that dispatches requests through sub. sub is expected to be the result of Pipeline.SubPipeline with client-only guards filtered out; this function does not validate its shape.

type QueryerSetter added in v1.6.4

type QueryerSetter interface {
	SetQueryer(q Queryer)
}

QueryerSetter is implemented by handlers that consume the internal-sub-pipeline Queryer (today: cache middleware for CNAME chase, resolver for NS A/AAAA and DNAME target).

type RecursionWorkKind added in v1.7.4

type RecursionWorkKind uint8

RecursionWorkKind identifies a separately bounded kind of recursive work.

const (
	RecursionWorkOutboundQuery RecursionWorkKind = iota
	RecursionWorkInternalQuery
	RecursionWorkDNSKEYCandidate
	RecursionWorkRRsetSignature
	RecursionWorkSignature
	RecursionWorkDSDigest
	RecursionWorkNSEC3Hash
	RecursionWorkConcurrentCrypto
)

type RecursionWorkLedger added in v1.7.4

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

func EnsureRecursionWork added in v1.7.4

func EnsureRecursionWork(ctx context.Context, policy RecursionWorkPolicy) (context.Context, *RecursionWorkLedger)

EnsureRecursionWork establishes one heap ledger beside ResponseMeta and pins it into the returned context. The first policy in a request tree wins.

func NewRecursionWorkLedger added in v1.7.4

func NewRecursionWorkLedger(policy RecursionWorkPolicy) *RecursionWorkLedger

NewRecursionWorkLedger constructs a ledger for policy. Callers normally use EnsureRecursionWork so every nested pipeline shares the ResponseMeta-owned instance.

func RecursionWorkFrom added in v1.7.4

func RecursionWorkFrom(ctx context.Context) *RecursionWorkLedger

RecursionWorkFrom returns the request-tree ledger, if one exists. The ResponseMeta fallback supports upstream response-writer contexts that were captured before a downstream resolver pinned the ledger directly.

func (*RecursionWorkLedger) CheckLocal added in v1.7.4

func (l *RecursionWorkLedger) CheckLocal(kind RecursionWorkKind, used uint32) error

CheckLocal checks a per-validation-object limit before the next item is examined. used is the number of items already examined: values below the limit are allowed, while used == limit is the first crossing. Local limits do not add to graph-wide counters.

func (*RecursionWorkLedger) Debit added in v1.7.4

Debit reserves one unit of kind. Shadow mode records crossings but always permits work. Enforce mode never lets an accepted counter exceed its cap and records the rejection as terminal provenance for the request tree. Work is never refunded: failed, cancelled, and losing attempts consumed the resource they started.

func (*RecursionWorkLedger) DebitBestEffort added in v1.7.4

func (l *RecursionWorkLedger) DebitBestEffort(kind RecursionWorkKind) error

DebitBestEffort reserves work for an optional branch. It shares the same counters and returns the same limit error to stop that branch, but a rejected debit does not make an otherwise successful request fail later via EnforcementError. Exhaustion remains visible in the tree snapshot and metrics.

func (*RecursionWorkLedger) EnforcementError added in v1.7.4

func (l *RecursionWorkLedger) EnforcementError() error

EnforcementError returns the first rejected dimension in enforce mode.

func (*RecursionWorkLedger) Reject added in v1.7.4

Reject records a governor failure that has no accepted-work counter, such as timing out while waiting for the resolver-wide crypto semaphore.

func (*RecursionWorkLedger) Retain added in v1.7.4

func (l *RecursionWorkLedger) Retain() (release func(), ok bool)

Retain keeps the request-tree ledger open for asynchronous work that can outlive the client-facing chain. The returned release function is safe to call more than once; callers must call it when that work is complete.

func (*RecursionWorkLedger) Snapshot added in v1.7.4

Snapshot returns the current accepted/observed work totals and crossings.

type RecursionWorkLimitError added in v1.7.4

type RecursionWorkLimitError struct {
	Kind  RecursionWorkKind
	Limit uint32
}

RecursionWorkLimitError reports which dimension rejected more work. Error deliberately remains stable on the wire; Kind and Limit are available to logs and tests without leaking policy details to clients.

func (*RecursionWorkLimitError) EDECode added in v1.7.4

func (e *RecursionWorkLimitError) EDECode() uint16

EDECode maps network-work enforcement to RFC 8914's finalized catch-all code and DNSSEC-work enforcement to DNSSEC Indeterminate.

func (*RecursionWorkLimitError) Error added in v1.7.4

func (e *RecursionWorkLimitError) Error() string

func (*RecursionWorkLimitError) Unwrap added in v1.7.4

func (e *RecursionWorkLimitError) Unwrap() error

type RecursionWorkMode added in v1.7.4

type RecursionWorkMode uint8

RecursionWorkMode controls whether request-tree work limits are disabled, observed without changing behaviour, or enforced.

const (
	RecursionWorkOff RecursionWorkMode = iota
	RecursionWorkShadow
	RecursionWorkEnforce
)

type RecursionWorkPolicy added in v1.7.4

type RecursionWorkPolicy struct {
	Mode                    RecursionWorkMode
	MaxOutboundQueries      uint32
	MaxInternalQueries      uint32
	MaxDNSKEYCandidates     uint32
	MaxRRsetSignatureChecks uint32
	MaxSignatureChecks      uint32
	MaxDSDigests            uint32
	MaxNSEC3Hashes          uint32
	MaxConcurrentCrypto     uint32
}

RecursionWorkPolicy is immutable after a ledger is created.

func MustRecursionWorkPolicyFromConfig added in v1.7.4

func MustRecursionWorkPolicyFromConfig(raw config.RecursionFirewallConfig) RecursionWorkPolicy

MustRecursionWorkPolicyFromConfig normalizes, validates, and translates the operator-facing recursion-firewall configuration. Resolver and pipeline construction both use this seam so programmatic callers cannot silently turn an unknown mode into a different security policy.

func (RecursionWorkPolicy) Enabled added in v1.7.4

func (p RecursionWorkPolicy) Enabled() bool

Enabled reports whether the policy should create and account a ledger.

type RecursionWorkSnapshot added in v1.7.4

type RecursionWorkSnapshot struct {
	Mode                          RecursionWorkMode
	OutboundQueries               uint32
	InternalQueries               uint32
	SignatureChecks               uint32
	DSDigests                     uint32
	NSEC3Hashes                   uint32
	OutboundExhausted             bool
	InternalExhausted             bool
	DNSKEYCandidatesExhausted     bool
	RRsetSignatureChecksExhausted bool
	SignatureChecksExhausted      bool
	DSDigestsExhausted            bool
	NSEC3HashesExhausted          bool
	ConcurrentCryptoExhausted     bool
	MaxOutboundQueries            uint32
	MaxInternalQueries            uint32
	MaxDNSKEYCandidates           uint32
	MaxRRsetSignatureChecks       uint32
	MaxSignatureChecks            uint32
	MaxDSDigests                  uint32
	MaxNSEC3Hashes                uint32
	MaxConcurrentCrypto           uint32
}

RecursionWorkSnapshot is a race-safe point-in-time ledger view.

type Registry added in v1.6.3

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

Registry collects middleware registrations and builds an immutable Pipeline from them. A Registry is safe for concurrent Register calls, but Build is expected to be called once.

func NewRegistry added in v1.6.3

func NewRegistry() *Registry

NewRegistry returns an empty Registry.

func (*Registry) Build added in v1.6.3

func (r *Registry) Build(cfg *config.Config) *Pipeline

Build runs every Constructor against cfg, skips disabled middlewares (typed-nil), and returns an immutable Pipeline. Constructors run outside the registry lock, so they may do heavy work (open files, spawn goroutines) without starving concurrent List calls.

func (*Registry) List added in v1.6.3

func (r *Registry) List() []string

List returns the registered middleware names in order.

func (*Registry) Register added in v1.6.3

func (r *Registry) Register(name string, c Constructor)

Register appends a middleware to the end of the registry.

func (*Registry) RegisterAt added in v1.6.3

func (r *Registry) RegisterAt(name string, c Constructor, idx int)

RegisterAt inserts a middleware at the given index. Out-of-range index panics.

func (*Registry) RegisterBefore added in v1.6.3

func (r *Registry) RegisterBefore(name string, c Constructor, before string)

RegisterBefore inserts a middleware immediately before the named one. Panics if `before` is not registered.

type Request added in v1.8.0

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

Request is the one request representation the chain carries. It is wire-first: a strict-path request is born from the raw packet with every fact the default chain needs parsed as offsets and scalars, and no decoded message exists until a handler asks for one through Chain.Materialize. A request born from an already decoded message (the internal sub-pipeline, DoH/DoQ, embedders, tests) starts with Msg set and the accessors read through it.

Raw borrows the transport job's receive buffer: it is valid until the job is released, which happens only after the middleware unwind completes. Nothing may retain it: the storage is reused for the next request.

Accessors report the client's original request facts. Materialization is one-way — wire → normalized → Msg — and never rewrites the parsed facts, so a handler that only needs the question or the EDNS shape never pays for decoding.

func NewRequest added in v1.8.0

func NewRequest(m *dns.Msg) *Request

NewRequest returns a message-born Request. Chain.Reset does this with pooled storage; this constructor serves callers building chains by hand.

func (*Request) AD added in v1.8.0

func (r *Request) AD() bool

AD reports the authenticated-data bit as the client sent it.

func (*Request) CD added in v1.8.0

func (r *Request) CD() bool

CD reports the checking-disabled bit.

func (*Request) ClientCookie added in v1.8.0

func (r *Request) ClientCookie() []byte

ClientCookie returns the raw client cookie half, or nil. Valid only for wire-born requests; the message path reads the cookie from the message.

func (*Request) CookieEcho added in v1.8.0

func (r *Request) CookieEcho() []byte

CookieEcho returns the full cookie option bytes the client sent — the client half plus any echoed server half — or nil. Valid only for wire-born requests.

func (*Request) DO added in v1.8.0

func (r *Request) DO() bool

DO reports the client's DNSSEC-OK bit.

func (*Request) EDNSVersion added in v1.8.0

func (r *Request) EDNSVersion() uint8

EDNSVersion returns the OPT version field (meaningful only with HasOPT).

func (*Request) EDNSWriterSlot added in v1.8.0

func (r *Request) EDNSWriterSlot() any

EDNSWriterSlot returns job-owned storage for the edns writer wrapper, if the transport offered one.

func (*Request) HasECS added in v1.8.0

func (r *Request) HasECS() bool

HasECS reports whether the request carried an EDNS client-subnet option.

func (*Request) HasNSID added in v1.8.0

func (r *Request) HasNSID() bool

HasNSID reports whether the request asked for NSID.

func (*Request) HasOPT added in v1.8.0

func (r *Request) HasOPT() bool

HasOPT reports whether the request carried an OPT record.

func (*Request) HasTCPKeepalive added in v1.8.0

func (r *Request) HasTCPKeepalive() bool

HasTCPKeepalive reports whether the request carried the RFC 7828 edns-tcp-keepalive option. Whether that means anything is the edns layer's call — the option is only honoured on a stream transport.

func (*Request) ID added in v1.8.0

func (r *Request) ID() uint16

ID returns the request's DNS message ID.

func (*Request) Msg added in v1.8.0

func (r *Request) Msg() *dns.Msg

Msg returns the decoded request, decoding a wire-born one on first call. It never returns nil for a request the chain accepted, so the mechanical migration from the old `ch.Request` (a *dns.Msg) is safe: reading the message costs a decode, it does not panic.

The decode alone is not the whole transition, though. A handler that goes on to call Next must not hand the job carrier to downstream handlers — Chain.Next detects a request materialized this way and detaches for them. A handler that needs the detached context for its own work (it starts recursion, derives a deadline, pins request-tree state) asks for both at once with Chain.Materialize.

func (*Request) Opcode added in v1.8.0

func (r *Request) Opcode() int

Opcode extracts the opcode.

func (*Request) ParseWire added in v1.8.0

func (r *Request) ParseWire(raw []byte, readTime time.Time, ednsSlot any) bool

ParseWire parses and validates raw into the request. It allocates nothing: every fact is a scalar or an offset into raw. Eligibility is conservative — exactly one question over an uncompressed name, empty answer/authority, at most one well-formed root OPT, plain query opcode — and false means the packet must take the decoded entry instead.

func (*Request) Qclass added in v1.8.0

func (r *Request) Qclass() uint16

Qclass returns the question class.

func (*Request) Qtype added in v1.8.0

func (r *Request) Qtype() uint16

Qtype returns the question type.

func (*Request) RD added in v1.8.0

func (r *Request) RD() bool

RD reports the recursion-desired bit.

func (*Request) Raw added in v1.8.0

func (r *Request) Raw() []byte

Raw returns the raw packet, or nil for a message-born request.

func (*Request) ReadTime added in v1.8.0

func (r *Request) ReadTime() time.Time

ReadTime returns the transport read time (zero for message-born requests).

func (*Request) RecordEDNSNormalization added in v1.8.0

func (r *Request) RecordEDNSNormalization(policy *ecs.Policy, client netip.Addr)

RecordEDNSNormalization is called by the edns handler's wire branch: it commits the facts materialization needs to normalize the request exactly as SetEdns0 would.

func (*Request) SetMsg added in v1.8.0

func (r *Request) SetMsg(m *dns.Msg)

SetMsg initializes the request from an already decoded message.

func (*Request) UDPSize added in v1.8.0

func (r *Request) UDPSize() uint16

UDPSize returns the client's advertised EDNS UDP size (0 without OPT).

func (*Request) Undecoded added in v1.8.0

func (r *Request) Undecoded() bool

Undecoded reports whether the request is still wire-only — no message has been built for it. It is the gate a handler puts in front of its wire branch: reading it never triggers a decode, where Msg does.

func (*Request) WireName added in v1.8.0

func (r *Request) WireName() []byte

WireName returns the question name's wire bytes (borrowed from Raw). Valid only for wire-born requests.

func (*Request) WireQuestionEnd added in v1.8.0

func (r *Request) WireQuestionEnd() int

WireQuestionEnd returns the offset just past the question section within Raw. Valid only for wire-born requests.

type ResolutionAttemptGuard added in v1.7.4

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

ResolutionAttemptGuard owns retry state and exact terminal request-local response provenance for one complete client request tree. A mutex keeps both maps atomic across resolver fan-out.

func EnsureResolutionAttemptGuard added in v1.7.4

func EnsureResolutionAttemptGuard(ctx context.Context) (context.Context, *ResolutionAttemptGuard)

EnsureResolutionAttemptGuard establishes and anchors the request tree's retry guard. Unlike recursion-work accounting, this RFC safeguard is always on.

func NewResolutionAttemptGuard added in v1.7.4

func NewResolutionAttemptGuard() *ResolutionAttemptGuard

NewResolutionAttemptGuard returns an empty request-tree retry guard.

func ResolutionAttemptGuardFrom added in v1.7.4

func ResolutionAttemptGuardFrom(ctx context.Context) *ResolutionAttemptGuard

ResolutionAttemptGuardFrom returns the request tree's retry guard.

func (*ResolutionAttemptGuard) Begin added in v1.7.4

func (g *ResolutionAttemptGuard) Begin(q dns.Question, endpoint, transport string) error

Begin records one actual network attempt, rejecting the fourth attempt for an identical RFC 9520 tuple. Call it immediately before other accounting and before dialing so a rejected attempt consumes neither work budget nor wire resources.

func (*ResolutionAttemptGuard) BeginCanonical added in v1.8.0

func (g *ResolutionAttemptGuard) BeginCanonical(q dns.Question, endpoint, transport string) error

BeginCanonical is Begin for a caller whose endpoint is already spelled the way CanonicalResolutionEndpoint would spell it — the delegation path, where the address was decoded from glue and its "IP:port" form was printed from that value once, at construction. Normalizing it again parses the string and prints an identical one back, per attempt.

The promise matters: a spelling that is not canonical splits one tuple's counter in two, and the RFC 9520 limit is what the counter enforces. Callers that cannot make the promise must use Begin.

type ResolutionAttemptLimitError added in v1.7.4

type ResolutionAttemptLimitError struct {
	Question  dns.Question
	Endpoint  string
	Transport string
}

ResolutionAttemptLimitError records the tuple rejected by the RFC 9520 request-tree attempt guard.

func (*ResolutionAttemptLimitError) Error added in v1.7.4

func (*ResolutionAttemptLimitError) Unwrap added in v1.7.4

func (e *ResolutionAttemptLimitError) Unwrap() error

Unwrap lets callers classify a limit error with errors.Is.

type ResolutionFailureStore added in v1.7.4

type ResolutionFailureStore interface {
	Store
	RecordZoneFailure(q dns.Question, zone string)
	ClearZoneFailure(q dns.Question, zone string)
}

ResolutionFailureStore is the optional RFC 9520 extension implemented by the built-in cache. A resolver records a zone only after every usable authority endpoint for that delegation failed; the cache can then suppress random-QNAME retries below the same failed zone (and the resulting parent/ancestor traffic) until the bounded backoff expires.

This stays separate from Store so plugins and test stores that only need ordinary answer caching do not have to implement failure-state policy.

type ResponseMeta added in v1.7.3

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

func ResponseMetaFrom added in v1.7.3

func ResponseMetaFrom(ctx context.Context) *ResponseMeta

ResponseMetaFrom returns the ctx's response metadata sink, or nil when none was established (background/priming work).

func WithForkedCut added in v1.8.0

func WithForkedCut(ctx context.Context) (context.Context, *ResponseMeta)

WithForkedCut returns ctx carrying a meta that keeps its own delegation-cut deadline while sharing the request tree's state, and that meta. See ForkCut for what the separation is for.

The returned meta is nil, and ctx unchanged, when ctx carries no meta to fork from — a caller with nothing to separate needs no scope.

func (*ResponseMeta) BoundCut added in v1.7.3

func (m *ResponseMeta) BoundCut(deadline time.Time)

(*ResponseMeta).BoundCut folds a delegation-cut deadline into the meta, keeping the earliest. Nil-safe and zero-ignoring so resolver call sites can invoke it unconditionally.

func (*ResponseMeta) BoundCutFor added in v1.7.3

func (m *ResponseMeta) BoundCutFor(deadline time.Time, key uint64)

BoundCutFor folds a delegation-cut deadline and its cache identity into the response metadata. The identity travels with the winning (earliest) deadline as one immutable atomic value, ready for the optional generation checks described by the Ghost/Phoenix durable design.

func (*ResponseMeta) ContextValue added in v1.7.4

func (m *ResponseMeta) ContextValue(key any) (any, bool)

ContextValue lets the root Chain expose its pooled ResponseMeta through a dedicated lazy server request context without another context.WithValue allocation. Exported WithResponseMeta retains normal derived-context isolation.

func (*ResponseMeta) Cut added in v1.7.3

func (m *ResponseMeta) Cut() (time.Time, uint64)

Cut returns the earliest delegation-cut deadline observed for the request tree and the delegation-cache key that supplied it. A zero deadline means unbounded; in that case the key is also zero.

func (*ResponseMeta) CutKey added in v1.7.3

func (m *ResponseMeta) CutKey() uint64

CutKey returns the delegation-cache key associated with CutUntil. It is meaningful only when CutUntil is non-zero.

func (*ResponseMeta) CutUntil added in v1.7.3

func (m *ResponseMeta) CutUntil() time.Time

CutUntil returns the earliest delegation-cut deadline observed for the request tree. Zero means unbounded.

func (*ResponseMeta) EnsureResolutionAttemptGuard added in v1.7.4

func (m *ResponseMeta) EnsureResolutionAttemptGuard() *ResolutionAttemptGuard

EnsureResolutionAttemptGuard returns the request tree's retry guard, installing one on first use.

func (*ResponseMeta) ForkCut added in v1.8.0

func (m *ResponseMeta) ForkCut() *ResponseMeta

ForkCut returns a meta that shares this one's request-tree state — work ledger, retry guard, failure and proof provenance — but accumulates its own delegation-cut deadline.

A sub-query's answer is cached under its own key, so it must be bounded by its own lineage and not by whatever asked for it: a nearly expired alias would otherwise store a freshly resolved target with seconds of life. The deriving request still inherits the sub-query's bound, by folding the fork's deadline back in once the sub-query has returned — the composed answer does contain the sub-query's records.

func (*ResponseMeta) IsCachedFailureResponse added in v1.7.4

func (m *ResponseMeta) IsCachedFailureResponse(msg *dns.Msg) bool

IsCachedFailureResponse reports whether msg is the exact response currently being emitted from the RFC 9520 failure cache for this request tree.

func (*ResponseMeta) MarkCachedFailureResponse added in v1.7.4

func (m *ResponseMeta) MarkCachedFailureResponse(msg *dns.Msg) func()

MarkCachedFailureResponse marks msg as a response currently being emitted from the RFC 9520 failure cache and returns an idempotent release function. Callers must keep the mark only around the synchronous WriteMsg call.

func (*ResponseMeta) RecursionWork added in v1.7.4

func (m *ResponseMeta) RecursionWork() *RecursionWorkLedger

RecursionWork returns the request tree's ledger, if accounting is active.

func (*ResponseMeta) Reset added in v1.7.3

func (m *ResponseMeta) Reset()

Reset clears request metadata before a pooled Chain is reused. Store(nil) is safe even if the ResponseMeta has previously been used; do not replace the struct by assignment because atomic values must not be copied after first use.

func (*ResponseMeta) ResolutionAttemptGuard added in v1.7.4

func (m *ResponseMeta) ResolutionAttemptGuard() *ResolutionAttemptGuard

ResolutionAttemptGuard returns the request tree's retry guard, if present.

type ResponseSizer added in v1.8.0

type ResponseSizer interface {
	Size() int
}

ResponseSizer is implemented by writers that can report a response's wire length without decoding it. It is deliberately separate from ResponseWriter: adding a method to that interface would break any plugin writer that implements it directly rather than by embedding.

type ResponseWriter added in v1.1.0

type ResponseWriter interface {
	Transport
	Msg() *dns.Msg
	Rcode() int
	Written() bool
	Proto() string
	RemoteIP() net.IP
	Internal() bool
}

ResponseWriter is the chain-side writer: the pooled wrapper every middleware sees, layered over a Transport.

type Sidecar added in v1.8.2

type Sidecar struct {
	Value any
}

Sidecar is policy state stamped beside a cache entry: an opaque value a policy middleware computed from the entry's own stored records. The cache carries and hands it back without reading Value.

The nil pointer is load-bearing: an absent sidecar means the entry was never evaluated — unknown, never clean. A policy layer that evaluated an entry and matched nothing must say so with a non-nil Sidecar, or every hit on that entry re-takes the decoded path forever.

type SidecarChain added in v1.8.2

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

SidecarChain carries one sidecar per chase segment, in chain order, as a bounded value — never a slice. The gate methods receive it by value, so a gated chase hit stays allocation-free: a slice here escapes through the interface call and puts a heap allocation on every cache-contained chase, which the zero-allocation hit contract forbids.

func (*SidecarChain) Append added in v1.8.2

func (c *SidecarChain) Append(sc *Sidecar) bool

Append adds the next segment's sidecar; it reports false when the chain is full (the caller's depth bound should make that impossible).

func (*SidecarChain) At added in v1.8.2

func (c *SidecarChain) At(i int) *Sidecar

At returns segment i's sidecar; nil means that segment is unevaluated.

func (*SidecarChain) Len added in v1.8.2

func (c *SidecarChain) Len() int

Len returns the number of segments carried.

type SidecarEvaluator added in v1.8.2

type SidecarEvaluator func(msg *dns.Msg) *Sidecar

SidecarEvaluator computes a Sidecar from one entry's stored message — the truth being admitted, after cacheability filtering, never the client's copy. It is called once per admitted entry through every admission door the store has. A nil result declines to evaluate and leaves the entry unknown.

The evaluator runs on admission paths (resolver completions, prefetch refreshes, the cache writer) and must be safe for concurrent use.

type SidecarPolicyProvider added in v1.8.2

type SidecarPolicyProvider interface {
	SidecarEvaluator() SidecarEvaluator
	WireHitGate() WireHitGate
}

SidecarPolicyProvider is implemented by the handler that owns response policy over stored answers (tomorrow: rpz). Either method may return nil to leave that half of the seam unwired.

type SidecarPolicySetter added in v1.8.2

type SidecarPolicySetter interface {
	SetSidecarPolicy(p SidecarPolicyProvider)
}

SidecarPolicySetter is implemented by the handler that owns the entry store (today: the cache middleware). Setup wires the first provider in pipeline order into every setter.

type StagedFlusher added in v1.8.0

type StagedFlusher interface {
	// FlushStaged sends everything staged so far. Must only be called
	// from the goroutine that serves this transport's requests.
	FlushStaged()
}

StagedFlusher is implemented by transports that stage replies for a batched send. The chain flushes them at the strict-path detach — the moment slow work is certain — so a reply already staged never waits behind an unrelated recursion.

type Store added in v1.6.4

type Store interface {
	Get(req *dns.Msg) (*dns.Msg, bool)
	// SetFromResponse stores resp keyed on (Question[0], keyCD).
	// cutUntil bounds the entry's effective lifetime to the
	// delegation cut that produced it (GHSA-mqfw-f48p-2vc8); zero
	// means unbounded.
	SetFromResponse(resp *dns.Msg, keyCD bool, cutUntil time.Time)
}

Store is the minimum cache facade a resolver sub-query needs. Satisfied by cache.Store; declared here so middleware.Setup can wire it from one handler into another without either importing the cache package.

type StoreProvider added in v1.6.4

type StoreProvider interface {
	Store() Store
}

StoreProvider is implemented by handlers that own a Store which should be shared with other handlers (today: the cache middleware; consumed by the resolver handler).

type StoreSetter added in v1.6.4

type StoreSetter interface {
	SetStore(s Store)
}

StoreSetter is implemented by handlers that consume a Store injected at Setup time (today: the resolver handler).

type Transport added in v1.8.0

type Transport interface {
	LocalAddr() net.Addr
	RemoteAddr() net.Addr
	WriteMsg(*dns.Msg) error
	Write([]byte) (int, error)
	Close() error
}

Transport is the transport-side writer contract: the surface a DNS transport — the SDNS-owned UDP/TCP/DoT jobs, the DoH and DoQ writers, mocks and buffer sinks — offers the chain. It is SDNS's own contract; the server does not serve through the miekg handler machinery.

type ValidatedDenial added in v1.7.4

type ValidatedDenial struct {
	DeniedName string
	Zone       string
	// Proof is the original response whose denial proof the resolver
	// authenticated. Middleware may propagate the provenance to a replacement
	// response, but consumers must derive cached proof material from this
	// source rather than from the replacement's possibly rewritten sections.
	Proof *dns.Msg
}

ValidatedDenial is resolver-authenticated NXDOMAIN provenance. It is carried out of band because neither RcodeNameError nor the AD bit proves that this resolver validated the denial itself.

Values are copied into ResponseMeta and never mutated there. DeniedName is the name proven not to exist; Zone is the signed zone that supplied the proof.

func ValidatedDenialForResponse added in v1.7.4

func ValidatedDenialForResponse(ctx context.Context, msg *dns.Msg) (ValidatedDenial, bool)

ValidatedDenialForResponse returns resolver-local validation provenance for this exact NXDOMAIN response. A copied or independently constructed message never inherits the mark.

type ValidatedNegativeProof added in v1.7.4

type ValidatedNegativeProof struct {
	Subject string
	Zone    string
	Kind    ValidatedNegativeProofKind
	Proof   *dns.Msg
	// Aggressive is true only when the stricter RFC 8198 evaluator
	// classified this exact response with the same RCODE. Exact DNSSEC
	// validation can legitimately succeed while aggressive reuse is forbidden
	// (notably NSEC3 Opt-Out); consumers publishing shared proof state must
	// require this bit in addition to local provenance.
	Aggressive bool
}

ValidatedNegativeProof is resolver-authenticated NXDOMAIN or NODATA provenance. Subject is the exact terminal (or QNAME-minimized) name whose denial semantics were checked; Zone is the validated signer zone. Proof is the original terminal response, never an outer alias response assembled later in the request tree.

The zero/Unknown Kind exists only so the older ValidatedDenial compatibility API can preserve its source contract. RFC 8198 admission requires an explicit NSEC or NSEC3 kind.

func ValidatedNegativeProofForResponse added in v1.7.4

func ValidatedNegativeProofForResponse(
	ctx context.Context,
	msg *dns.Msg,
) (ValidatedNegativeProof, bool)

ValidatedNegativeProofForResponse returns resolver-local validation provenance for this exact NXDOMAIN or NODATA response. AD alone, a message copy, or an independently produced negative response never inherits it.

type ValidatedNegativeProofKind added in v1.7.4

type ValidatedNegativeProofKind uint8

ValidatedNegativeProofKind identifies the denial mechanism the resolver authenticated. Keeping this in the provenance prevents a later cache layer from combining NSEC and NSEC3 records, or choosing a different NSEC3 chain than the one whose semantics were checked.

const (
	ValidatedNegativeProofUnknown ValidatedNegativeProofKind = iota
	ValidatedNegativeProofNSEC
	ValidatedNegativeProofNSEC3
)

type WireBodyLeaser added in v1.8.0

type WireBodyLeaser interface {
	BeginWire(size, reserve int) []byte
	CommitWire(body []byte, info WireInfo) error
	AbortWire()
}

WireBodyLeaser is the pre-build lease contract: a caller that knows the response size asks the writer for the buffer the body will be built in, so the bytes are born where the transport wants them (the server's job slab on the strict path) instead of being allocated by the producer.

BeginWire returns a zero-length slice with capacity size+reserve, or nil when the writer cannot lease (the caller falls back to building its own body for WriteWire). Exactly one of CommitWire or AbortWire must follow every successful BeginWire, before any other write on this writer. CommitWire has WriteWire's semantics — including the lazy post-write Msg() retention, which is why the lease belongs to the writer until the request finishes, never returning to any pool at commit time (the #558 lesson). AbortWire releases the lease without a send; the caller may then still answer through the ordinary Msg path.

type WireCapability added in v1.8.0

type WireCapability struct {
	// DO is the client's own DNSSEC-OK bit. It cannot be read from the
	// request at serve time: the edns layer sets DO on the request's OPT
	// so upstream validation happens regardless of what the client asked
	// for, and only the writer still remembers the original.
	DO bool
	// Reserve is how many bytes the chain will append below this point
	// (the per-client OPT).
	Reserve int
	// MaxSize caps the reply the transport accepts; 0 means unbounded.
	MaxSize int
}

WireCapability is what the writer chain reports about serving bytes for the request in flight. It is gathered before any body is produced, so a request that cannot take the byte path never pays for it.

type WireHitGate added in v1.8.2

type WireHitGate interface {
	// JudgeWireHit judges a single-entry byte serve. sc is the entry's
	// sidecar; nil means unevaluated.
	JudgeWireHit(sc *Sidecar) WireHitVerdict
	// JudgeWireChase judges a composed chase, one sidecar per segment in
	// chain order. Any nil element is an unevaluated segment.
	JudgeWireChase(sidecars SidecarChain) WireHitVerdict
	// CountWireHit records the policy outcome of a byte-served exact
	// hit. Called once, after the transport accepted the bytes; no later
	// layer decodes them, so this is the only place the outcome exists.
	CountWireHit(sc *Sidecar)
	// CountWireChase is CountWireHit for a committed chase composition.
	CountWireChase(sidecars SidecarChain)
}

WireHitGate judges record-bearing byte serves. When a gate is wired, the cache consults it before serving stored bytes for an exact hit or a composed CNAME chase; any verdict but WireHitServe declines the byte serve and the same query is answered by the decoded path instead — where the policy layer's own response writer sees a full message. Composite denial classes (subtree cuts, failure state) carry no stored records and are never gated.

The Judge methods are deterministic decisions over the sidecars and — for a per-query gate obtained through QueryPolicyGate — that query's own policy state. A per-query gate may memoize the decision it judged; the matching Count method then records exactly that decision, which is what keeps a judge/commit pair coherent across a concurrent policy reload. Accounting fires exactly once per byte-served hit, after the bytes were committed to the transport — the only point where a byte serve can no longer fall back to the decoded path and be counted twice. Queries whose policy work must all happen on the decoded path are steered off the byte path by the policy writer withholding its wire capability.

type WireHitVerdict added in v1.8.2

type WireHitVerdict uint8

WireHitVerdict is a gate's judgment of one byte serve.

const (
	// WireHitServe permits the byte serve. Nothing is counted here — a
	// serve can still decline past the gate (writer readiness, build,
	// transport fallback) and land on the decoded path, so accounting
	// waits for the committed-bytes callback below.
	WireHitServe WireHitVerdict = iota
	// WireHitDecode sends the query to the decoded path because policy
	// wants the full message there. The sidecar itself was usable; the
	// cache changes nothing about it.
	WireHitDecode
	// WireHitRestamp sends the query to the decoded path because the
	// sidecar is unusable — unevaluated, or stamped under a generation
	// the gate no longer accepts. The decoded serve re-evaluates the
	// entry's records and restamps over the judged pointer, so the entry
	// rejoins the byte path instead of decoding until eviction.
	WireHitRestamp
)

type WireInfo added in v1.8.0

type WireInfo struct {
	// Rcode of the packed response.
	Rcode int
	// AuthenticatedData mirrors the AD bit currently set in the body.
	AuthenticatedData bool
	// HasDNSSEC reports whether the body carries DNSSEC records the
	// client did not ask for — the fact the edns layer needs for its
	// DO=0 decision. An explicit RRSIG query's answer is its payload,
	// not augmentation, and travels with this unset.
	HasDNSSEC bool
	// HasEDE carries an RFC 8914 Extended DNS Error for the edns layer
	// to append to the reply OPT. A client without EDNS never receives
	// it — exactly the Msg path's re-attach behavior.
	HasEDE  bool
	EDECode uint16
	EDEText string
}

WireInfo carries the response facts observers and shaping layers need when a response travels as packed bytes. It replaces the field reads they would otherwise perform on a *dns.Msg.

type WireTransportLeaser added in v1.8.0

type WireTransportLeaser interface {
	LeaseWire(capacity int) []byte
}

WireTransportLeaser is implemented by transport writers whose reply buffer lives in job-owned storage: the base response writer's BeginWire leases from it instead of allocating. nil means the transport cannot lease this size and the caller falls back.

type WireWriter added in v1.8.0

type WireWriter interface {
	WireReady() (WireCapability, bool)
	WriteWire(body []byte, info WireInfo) error
}

WireWriter is the optional byte-serving contract. A response written through WriteWire carries no OPT record; the edns layer appends the per-client OPT it would have attached on the Msg path.

WireReady is the allocation-free preflight: it walks the whole chain and reports both whether every layer can shape bytes and the facts the caller needs to decide. Callers must consult it before building a body — a late refusal would mean paying for both paths.

WriteWire keeps ErrWireFallback as a defensive backstop for conditions a preflight cannot foresee; it must still return before writing any byte.

Directories

Path Synopsis
Package defaults holds SDNS's default middleware chain, in its default order.
Package defaults holds SDNS's default middleware chain, in its default order.
Package dns64 implements RFC 6147 DNS64.
Package dns64 implements RFC 6147 DNS64.
Package hostsfile implements a high-performance hosts file resolver with advanced features like wildcard support and automatic reloading
Package hostsfile implements a high-performance hosts file resolver with advanced features like wildcard support and automatic reloading
Package kubernetes - Kubernetes API client
Package kubernetes - Kubernetes API client
Package reflex detects DNS amplification/reflection attacks.
Package reflex detects DNS amplification/reflection attacks.
dnssec
Package dnssec implements pure DNSSEC verification primitives: RRSIG/DS validation, NSEC and NSEC3 denial-of-existence proofs, and the EDE-coded sentinel errors they return.
Package dnssec implements pure DNSSEC verification primitives: RRSIG/DS validation, NSEC and NSEC3 denial-of-existence proofs, and the EDE-coded sentinel errors they return.
localroot
Package localroot maintains a local, verified copy of the root zone (RFC 8806) and answers the three questions the resolver's walk would otherwise ask a root server: where a TLD's delegation lives, whether a TLD's DS exists, and the signed proof that a TLD does not exist.
Package localroot maintains a local, verified copy of the root zone (RFC 8806) and answers the three questions the resolver's walk would otherwise ask a root server: where a TLD's delegation lives, whether a TLD's DS exists, and the signed proof that a TLD does not exist.
localroot/roottest
Package roottest builds a miniature signed root zone for tests: an apex with SOA/NS/DNSKEY/NSEC sealed by a ZONEMD, a signed delegation (com., with DS), an unsigned delegation (org., NSEC without the DS bit), and in-zone glue.
Package roottest builds a miniature signed root zone for tests: an apex with SOA/NS/DNSKEY/NSEC sealed by a ZONEMD, a signed delegation (com., with DS), an unsigned delegation (org., NSEC without the DS bit), and in-zone glue.
Package rpz is the Response Policy Zones middleware: the chain seat of the internal/rpz engine.
Package rpz is the Response Policy Zones middleware: the chain seat of the internal/rpz engine.
Package views serves per-client static answers for configured zones.
Package views serves per-client static answers for configured zones.

Jump to

Keyboard shortcuts

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