Middleware
The handler contract, the chain, and where a new middleware goes in it.
Every query is served by a chain of middlewares. Each is a Handler:
type Handler interface {
// Name must match the name used in Register so Pipeline.Get
// can resolve the handler back.
Name() string
// ServeDNS processes a query. Call ch.Next to continue the chain,
// or ch.Cancel / ch.CancelWithRcode to stop it.
ServeDNS(ctx context.Context, ch *Chain)
}
A middleware either answers the query and stops the chain, or modifies something and continues it:
ch.Next(ctx) // continue
ch.Cancel() // stop, writing nothing further
ch.CancelWithRcode(rcode, do) // write a reply with this rcode, then stop
Only Next takes the context. do on CancelWithRcode sets the DO bit on the
response’s OPT record — it is not an authoritative flag.
The default chain
recovery → metrics → dnstap → accesslist → ratelimit → reflex → edns
→ accesslog → chaos → hostsfile → views → blocklist → rpz → as112
→ kubernetes → dns64 → cache → failover → resolver → forwarder
The order lives in middleware/defaults, which is generated. It is a package
rather than an init in main specifically so that everything needing the real
chain can ask for it — the binary, the benchmarks, a test harness. When the list
lived in main each of those kept its own copy, and they drifted; a benchmark
quietly measuring five handlers fewer than production still reports a number.
RegisterUpTo(name) registers the chain up to but not including name, which
is how a harness replaces the tail with a stub while keeping everything ahead of
it real.
Registering
func init() {
middleware.Register("myfilter", func(cfg *config.Config) middleware.Handler {
return New(cfg)
})
}
Register appends to the end. When placement matters:
RegisterAt(name, ctor, idx)— at an index; out of range panics.RegisterBefore(name, ctor, before)— immediately before a named middleware; panics if the target is not registered.
Registering a name twice panics. That is intentional: a duplicate name means
Pipeline.Get cannot resolve the handler, and failing at startup is better than
resolving to the wrong one.
Where to put a new middleware
Position is a design decision, not a detail:
- Before
cacheif it must see queries the cache would otherwise answer. Policy and filtering belong here — this is where dynamic plugins are inserted. Note that this is not “every query”: access control, rate limiting, the hosts file, views, the blocklist and RPZ all run earlier and any of them can end the chain first. - After
cacheif it only concerns queries that actually need resolving. - Before
accesslistessentially never; nothing should run ahead of access control except recovery and instrumentation.
Self-declaring middleware
A middleware that needs wiring beyond construction declares it through a marker
or setter interface that middleware.Setup looks for, rather than by adding a
branch to the server’s startup code. Keeping the knowledge in the middleware is
what lets the chain stay a list.
Testing one
Build a chain with just your handler and a stub behind it, feed it a
*dns.Msg, and assert on what came back. middleware.HandlerFunc adapts a
plain function into a Handler, which is enough for the stub.
No assertion library, and no network — see Building and testing.