coredns/coredns
Patterns and conventions
This page collects the conventions every plugin and core package follows. New code that ignores these patterns will fail review.
Plugin layout
Every in-tree plugin lives at plugin/<name>/ and follows a consistent layout:
plugin/<name>/
├── README.md # User-facing docs (also rendered to man/coredns-<name>.7)
├── setup.go # plugin.Register("name", setup) and Caddyfile parsing
├── <name>.go # Handler with ServeDNS
├── *_test.go # Unit tests
└── (optional)
├── metrics.go # Prometheus collectors
├── log_test.go # log assertions
├── fuzz.go # Fuzz harnessThe package name matches the directory (e.g. package forward for plugin/forward/). Names are lowercase, no underscores in directory names except a small set already grandfathered in (grpc_server, k8s_external).
A new plugin must also be added to plugin.cfg so directives_generate.go includes it. The position in the file is its execution position in the chain.
Plugin registration
A plugin's init() calls plugin.Register (which is a re-export of caddy.RegisterPlugin):
func init() {
plugin.Register("forward", setup)
}setup(c *caddy.Controller) error parses Caddyfile arguments via c.Next(), c.Val(), c.RemainingArgs(), and c.NextBlock(). Errors must be wrapped with plugin.Error("name", err) so the diagnostic includes the plugin name.
The setup function appends to the active plugin chain:
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
f.Next = next
return f
})If a plugin can only be configured once per server block, it returns plugin.ErrOnce on the second invocation.
The Handler interface
type Handler interface {
ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error)
Name() string
}The contract from plugin/plugin.go:
ServeDNSreturns(rcode, err). Iferr != nil, CoreDNS responds SERVFAIL.- The rcode tells CoreDNS whether the plugin already wrote to the client.
SERVFAIL,REFUSED,FORMERR,NOTIMPmean no reply was written. Any other value means a reply has been written. - To delegate to the next plugin, call
plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r). It also starts an OpenTracing child span and wraps the writer in apluginWriterso metrics attribute the response to the plugin that wrote it. - Plugins that mutate a response must
Copy()it first. Thedns.Msgfrom a downstream plugin may be backed by zone data and mutating it corrupts that data.
Logging
Use plugin/pkg/log with the plugin name attached:
import clog "github.com/coredns/coredns/plugin/pkg/log"
var log = clog.NewWithPlugin("forward")
log.Info("started") // [INFO] plugin/forward: started
log.Errorf("upstream %s: %v", a, err)
log.Debug("only when debug plugin is loaded")Levels are Info, Warning, Error, Debug (each has a *f variant). Debug output requires the debug plugin in the Corefile. From plugin.md: keep logging to a minimum and prefer returning errors so an upstream errors plugin can format them consistently.
Errors
- Errors returned from
setupmust be wrapped withplugin.Error(name, err). - Errors returned from
ServeDNSpropagate to theerrorsplugin, which logs them. - Constants
ErrOnce,ErrNoHealthy, etc. are defined per-plugin where it makes sense.
Metrics
- The Prometheus namespace is
plugin.Namespace(="coredns"). - The subsystem is the plugin name. So a counter for
cachebecomescoredns_cache_…. - Histograms reuse
plugin.TimeBuckets(16 exponential buckets from 0.25ms to 8s) orplugin.SlimTimeBucketsfor low-cardinality cases. - Native histogram bucket factor is
plugin.NativeHistogramBucketFactor(1.05). - Register collectors with
promautoso they're added to the default registry. Theprometheusplugin (plugin/metrics/) reads from that registry. - Each plugin's
README.mdshould have a## Metricssection listing its exposed metrics.
Readiness
If a plugin can be temporarily unhealthy, implement ready.Readiness:
func (p *Plugin) Ready() bool { /* ... */ }The ready plugin returns 200 only when every registered readiness check passes.
Sockets
Don't call net.Listen directly. Use plugin/pkg/reuseport.Listen and ListenPacket so reload events can transfer sockets between processes.
Context values
Plugins may read these context values:
| Key | Type | Source | Notes |
|---|---|---|---|
dnsserver.Key{} |
*Server |
Server.ServeDNS |
Used by metrics to identify the server |
dnsserver.LoopKey{} |
int |
Server.ServeDNS |
Loop detection counter for the file plugin |
dnsserver.ViewKey{} |
string |
Server.ServeDNS |
The view name when the matched config has one |
dnsserver.HTTPRequestKey{} |
*http.Request |
ServerHTTPS.ServeHTTP |
Original DoH request |
Fallthrough
When a plugin is responsible for only a subset of the names within its zone, it should accept a fallthrough directive that lists optional zones. The shared plugin/pkg/fallthrough package parses and matches these zones. The convention: the plugin handles names it owns and calls the next plugin for everything else.
Documentation style
Per plugin.md, plugin READMEs follow the Unix man-page style:
- Plugin names in prose are italicised:
*forward*. - User-supplied arguments are uppercase, in code spans when literal:
**FROM**,**TO**. - Optional text in block quotes:
[optional]. - Variable-length arguments use
...:arg.... - Examples must use
example.orgorexample.net.
The README must contain ## Name, ## Description, ## Syntax, ## Examples. Metrics, Ready, and Bugs are added when applicable. The make.doc workflow regenerates man/coredns-<name>.7 from the README on every PR.
Coding style
.golangci.yml enables a focused linter set. The relevant rules:
gofmtis the source of truth for formatting.reviveenforcescontext-as-argument,error-naming,error-strings,errorf,early-return,superfluous-else,range,unused-parameter,use-any. Tests are exempt fromgosecandperfsprint.staticcheck,govet/nilness,unused,prealloc,wastedassign,intrange,modernizeround out the bug-finding set.nakedretandwhitespacekeep returns explicit and prevent stray blank lines.
Conventional small things:
- Configuration parse functions are named
parse(notparseFooConfig). - The plugin's main type is named after the plugin:
Forward,Cache,Hosts. - Package comments live above
packageand explain what the plugin does. - Logged time durations should be in seconds (
d.Seconds()).
Testing conventions
- Unit tests sit next to the code (
foo_test.go). - Integration tests live in
test/. They usetest.Serverfromtest/server.goto drive a Corefile end-to-end. - Tests must use
example.org/example.netdomains and ephemeral ports (:0). - Race detection is enabled in CI for unit tests; some tests need
-raceto surface goroutine bugs (especially incache,forward,kubernetes). - Fuzz harnesses live in
*/fuzz.goand run via thecifuzz.ymlworkflow. - Tests that require network access or external services are guarded behind environment variables.
Mutating a response
Quoting plugin.md:
If a plugin mutates a response it MUST make a copy of the entire response before doing so. A response is a pointer to a
dns.Msgand as such you will be manipulating the original response, which could have been generated from a data store.
Mutating plugins (e.g. rewrite, cache's ResponseWriter) wrap the downstream dns.ResponseWriter and override WriteMsg to call res.Copy() first.
DCO sign-off
Every commit must carry a Signed-off-by trailer. Use git commit -s. Probot enforces this on PRs. See CONTRIBUTING.md.
Adding a new plugin to the main repo
A new plugin is roughly 1000 lines. Per CONTRIBUTING.md the recommended sequence is:
- Open a PR with just the
README.mdto settle the name and configuration surface. - Follow up with
setup.goand theServeDNShandler. - Add tests, metrics, and (if applicable) readiness.
- Add the plugin to
plugin.cfgin the right position; runmake gen. - Update
CODEOWNERS(or rely onowners_generate.go).
plugin.md's "Qualifying for Main Repo" section lists the criteria: useful to others, sufficiently distinct, supports IPv4 and IPv6, has tests, has a README.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.