coredns/coredns
Plugin system
Active contributors: miekg, chrisohaver, johnbelamaric
Purpose
The plugin system is the contract between core and the 60+ plugins shipped under plugin/. It defines what a plugin must implement (Handler), how plugins are chained (Plugin = func(Handler) Handler), how a plugin delegates to the next plugin (NextOrFailure), and how response writes are tracked for metrics and tracing.
This page covers the small plugin/plugin.go file and the registration mechanics. The catalog of plugins lives in Plugins overview.
Directory layout
plugin/
├── plugin.go # Plugin/Handler interfaces, NextOrFailure, ClientWrite, Namespace
├── plugin_test.go
├── register.go # plugin.Register: alias for caddy.RegisterPlugin
├── normalize.go # Name, Zones, SplitHostPort, Namespace helpers
├── normalize_test.go
├── backend.go # ServiceBackend interface for etcd/k8s/route53/...
├── backend_lookup.go # Shared backend lookup logic
├── done.go # done sentinel error
├── log_test.go
└── pkg/ # Shared utility packages (see Shared packages)The two interfaces
plugin/plugin.go defines exactly two types every plugin author cares about:
type Plugin func(Handler) Handler
type Handler interface {
ServeDNS(context.Context, dns.ResponseWriter, *dns.Msg) (int, error)
Name() string
}Plugin is a constructor function. When dnsserver.NewServer folds the chain right-to-left, it calls each Plugin with the next handler so plugins can capture their successor. Plugin code typically returns this constructor from setup:
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
f.Next = next
return f
})Handler is the runtime interface. ServeDNS returns (rcode, err) where:
err != nil→ CoreDNS responds SERVFAIL.rcodeindicates whether the plugin already wrote to the client. CoreDNS treats SERVFAIL, REFUSED, FORMERR, NOTIMP as "no reply written"; any other rcode means the reply has been written.
HandlerFunc is provided as a convenience for tests and one-off handlers; its Name() returns "handlerfunc".
Chain compilation
graph LR
P3 -->|next nil| P3End[end]
P2 -->|next P3| P3
P1 -->|next P2| P2
Server -->|ServeDNS| P1Inside dnsserver.NewServer:
var stack plugin.Handler
for i := len(site.Plugin) - 1; i >= 0; i-- {
stack = site.Plugin[i](stack)
site.registerHandler(stack)
}
site.pluginChain = stackFolding right-to-left means the last plugin in directive order is constructed with next == nil. Any earlier plugin captures the previous result. The variable stack ends up holding the head of the chain.
This loop also has side effects:
registerHandler(stack)records the plugin under itsName()soConfig.Handler(name)can look it up.- The first
MetadataCollector(typicallyplugin/metadata) is bookmarked on the config. - The
traceplugin is bookmarked soServer.Tracer()can return its tracer. - Plugins listed in
EnableChaos(chaos,forward,proxy) flipServer.classChaos = true.
Delegating to the next plugin
func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error)This helper is used by every plugin that wants to fall through. It does three things:
- If
next == nil, returnsdns.RcodeServerFailureandError(name, "no next plugin found"). - If a tracer span is active in
ctx, starts a child span named after the next plugin so requests can be traced through the chain. - Wraps
win apluginWriterthat records the next plugin's name on the innerPluginTracker(implemented by the cache and metrics writers). When the next plugin actually writes, the metrics plugin attributes the response to it.
Response writer tracking
pluginWriter is private but the PluginTracker interface is exported:
type PluginTracker interface {
SetPlugin(name string)
GetPlugin() string
}The plugin/metrics plugin's recorder implements this interface. As the chain unwinds, each NextOrFailure overwrites the recorded plugin name, so the final value is whichever plugin actually called WriteMsg. That name shows up in the coredns_dns_responses_total metric labels.
pluginWriter overrides every method on dns.ResponseWriter: WriteMsg, Write, LocalAddr, RemoteAddr, Close, TsigStatus, TsigTimersOnly, Hijack. The first two also call tracker.SetPlugin(pw.plugin).
Plugin registration
plugin/register.go is one line:
var Register = caddy.RegisterPluginPlugins call plugin.Register("name", setup) from init():
func init() {
plugin.Register("forward", setup)
}For the registration to take effect, the plugin must be blank-imported by the binary. The blank import lives in core/plugin/zplugin.go, generated from plugin.cfg. Caddy then drives the directive lifecycle in the order from zdirectives.go.
Helpers in plugin.go
| Symbol | Purpose |
|---|---|
Error(name, err) |
Wraps err as plugin/<name>: <msg>. Used in setup errors. |
ClientWrite(rcode) |
True if a non-error rcode means the client already received a reply. |
ErrOnce |
Returned from setup when a plugin can only be configured once per server block. |
Namespace |
Prometheus namespace string "coredns". |
TimeBuckets / SlimTimeBuckets |
Standard Prometheus histogram buckets. |
NativeHistogramBucketFactor |
1.05; default for native histograms. |
plugin/normalize.go adds:
| Symbol | Purpose |
|---|---|
Name |
A type representing a DNS name with Matches(child) for zone-suffix tests. |
Zones |
A slice of zone names; Matches(qname) returns the longest matching zone. |
Host |
Wrapper for host:port strings with normalization helpers. |
SplitHostPort |
Used by dnsContext.InspectServerBlocks to split keys, including CIDR expansion. |
Backend plugins
plugin/backend.go and plugin/backend_lookup.go define the ServiceBackend interface and shared lookup logic used by plugins that resolve names from a key/value backend (etcd, kubernetes, route53, azure, clouddns, nomad, k8s_external). Each backend implements Services(state, exact, opt), Reverse, Lookup, IsNameError, etc. and the shared Lookup walks A/AAAA/SRV/PTR types to build a unified response. See Backends.
Key abstractions
| Symbol | File | Description |
|---|---|---|
Plugin |
plugin/plugin.go |
Constructor function func(Handler) Handler |
Handler |
plugin/plugin.go |
Runtime DNS handler interface |
HandlerFunc |
plugin/plugin.go |
Adapter that turns a func into a Handler |
NextOrFailure |
plugin/plugin.go |
Delegate to the next plugin with tracing and response tracking |
ClientWrite |
plugin/plugin.go |
Decide whether the server must synthesize an error reply |
pluginWriter / PluginTracker |
plugin/plugin.go |
Track which plugin actually wrote the response |
Register |
plugin/register.go |
Alias for caddy.RegisterPlugin |
ServiceBackend |
plugin/backend.go |
Common interface for backend-style plugins |
Integration points
- Caddy. Plugin registration is
caddy.RegisterPlugin. The directive lifecycle is driven by Caddy'sdnsserver type. - Tracing.
NextOrFailurereads the active span fromot.SpanFromContext(ctx)and starts a child. Thetraceplugin installs the tracer onServer. - Metrics.
pluginWriter.SetPluginis the bridge toplugin/metricsfor response attribution. - Backend plugins.
plugin/backend_lookup.goprovides shared lookup logic; backend plugins satisfyServiceBackend.
Entry points for modification
- Adding a new core plugin contract method → extend
Handler. Avoid: this would force every plugin to update. - New helper for plugin authors → put it in
plugin/pkg/<name>/. See Shared packages. - Changing how response writes are attributed →
pluginWriter.WriteMsg/Write. - New mode for
plugin.Error(e.g. structured errors) →plugin/plugin.go.
For plugin (not plugin-system) modifications, start in Plugins overview and the relevant category page.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.