Introduction To CoreDNS
CoreDNS is a powerful, flexible DNS server written in Go. One of its key features is its plugin-based architecture, which allows users to extend its functionality easily. In this blog post, we’ll explore how to write custom plugins for CoreDNS.
As previously mentioned, CoreDNS utilizes a plugin chain architecture, enabling you to stack multiple plugins that execute sequentially. Most of CoreDNS’s functionality is provided by its built-in plugins. You can explore these bundled plugins by Clicking here.
Architecture Overview
CoreDNS follows a similar approach to Caddy, as it is based on Caddy v1:
-
Load Configuration: Configuration is loaded through the
Corefile
file. -
Plugin Setup: Plugins must implement a
setup
function to load, validate the configuration, and initialize the plugin. -
Handler Implementation: You need to implement the required functions from the
plugin.Handler
interface. -
Integrate Your Plugin: Add your plugin to CoreDNS by either including it in the
plugin.cfg
file or by wrapping everything in an external source code. Further details can be found below.
Develop
Configuration
As mentioned above, everything is done through the Corefile
. If you’re not familiar with the syntax, check this short explanation: https://coredns.io/2017/07/23/corefile-explained/
. { foo
}
In the example above, .
defines a server block, and foo
is the name of your plugin. You can specify a port or add arguments to your plugin.
.:5353 { foo bar
}
Now CoreDNS is running on port 5353
and my plugin named foo
is given the argument bar
.
It’s useful to enable the plugins
log
anddebug
during the development.
Checkout the list of bundled plugins to figure out which ones you need in your setup: https://coredns.io/plugins/
Setup
The first thing you need to do is to register and set up your plugin. Registration is done through a function called init
which you need to include in your go module.
package foo import ( "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin"
) func init() { plugin.Register("foo", setup)
}
Now we need to implement setup()
which parses the configuration and returns our initialized plugin.
package foo import ( "github.com/coredns/caddy" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin"
) func init() { plugin.Register("foo", setup)
} func setup(c *caddy.Controller) error { c.Next() // #1 if !c.NextArg() { // #2 return c.ArgErr() } dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { return Foo{Next: next, Bar: c.val()} // #3 }) return nil // #4
}
- Skip the first token which is
foo
, the name of our argument. - Return an error if our argument didn’t have any value.
- Put the value of our argument
bar
in the plugin struct and return it to be put in the plugin chain. Read more details on the plugin struct further down. - return
nil
as an error if everything is good to go.
Handler
All plugins need to implement plugin.Handler
which is the entry point to your plugin.
First, we need to write a struct containing the necessary arguments, runtime objects, and also the next plugin in the chain.
type Foo struct { Bar string Next plugin.Handler
}
This is the actual struct that we created in the previous step.
We also need a method to return the name of the plugin.
func (h Foo) Name() string { return "foo" }
Now it’s time for the most important method which is ServeDNS()
. This is the method that is called for every DNS query routed to your plugin. You can also generate a response here making your plugin work as a data backend.
func (h Foo) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { return h.Next.ServeDNS(ctx, w, r) }
What you see here does nothing but call the next plugin in the chain. But we don’t have to do that 🙂
Use r *dns.Msg
to get some info on the DNS query.
state := request.Request{W: w, Req: r}
qname := state.Name()
List of variables you can get from state
:
-
state.Name()
name of the query – includes the zone as well -
state.Type()
type of the query – e.g.A
,AAAA
, etc -
state.Ip()
IP address of the client making the request -
state.Proto()
transport protocol –tcp
orudp
-
state.Family()
IP version –1
for IPv4 and2
for IPv6
> Read the following file for the complete list: https://github.com/coredns/coredns/blob/master/request/request.go
You can also generate a response and return from the chain. For that, you need to use the amazing github.com/miekg/dns
package and build a dns.Msg
to return.
func (h Foo) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { dummy_ip := "1.1.1.1" state := request.Request{W: w, Req: r} qname := state.Name() answers := make([]dns.RR, 0, 10) resp := new(dns.A) resp.Hdr = dns.RR_Header{Name: dns.Fqdn(qname), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: a.Ttl} resp.A = net.ParseIP(dummy_ip) answers = append(answers, resp) m := new(dns.Msg) m.SetReply(r) m.Authoritative, m.RecursionAvailable, m.Compress = true, false, true m.Answer = append(m.Answer, answers...) state.SizeAndDo(m) m = state.Scrub(m) _ = w.WriteMsg(m) return dns.RcodeSuccess, nil
}
In the example shown above, we create an A
record response and return it to the client.
Check out the DNS package we used for more details on how to create DNS objects: https://pkg.go.dev/github.com/miekg/dns
If successful, we return dns.RcodeSuccess
. To see more return codes, check out here: https://pkg.go.dev/github.com/miekg/dns#pkg-constants
A few important return codes:
-
RcodeSuccess
: No error -
RcodeServerFailure
: Server failure -
RcodeNameError
: Domain doesn’t exist -
RcodeNotImplemented
: Record type not implemented
Logging
You can use the logging package provided by the CoreDNS itself, github.com/coredns/coredns/plugin/pkg/log
.
package Foo import ( clog "github.com/coredns/coredns/plugin/pkg/log"
) var log = clog.NewWithPlugin("foo")
Now you can log anything you need with different levels:
log.Info("info log")
log.Debug("debug log")
log.Warning("warning log")
log.Error("error log")
Final Example
package Foo import ( "github.com/coredns/coredns/request" "github.com/miekg/dns" "golang.org/x/net/context" clog "github.com/coredns/coredns/plugin/pkg/log"
) var log = clog.NewWithPlugin("foo") type Foo struct { Bar string Next plugin.Handler
} func (h Foo) Name() string { return "foo" } func (h Foo) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { dummy_ip := "1.1.1.1" state := request.Request{W: w, Req: r} qname := state.Name() answers := make([]dns.RR, 0, 10) resp := new(dns.A) resp.Hdr = dns.RR_Header{Name: dns.Fqdn(qname), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: a.Ttl} resp.A = net.ParseIP(dummy_ip) answers = append(answers, resp) log.Debug("answers created") m := new(dns.Msg) m.SetReply(r) m.Authoritative, m.RecursionAvailable, m.Compress = true, false, true m.Answer = append(m.Answer, answers...) state.SizeAndDo(m) m = state.Scrub(m) _ = w.WriteMsg(m) return dns.RcodeSuccess, nil
}
Compile
CoreDNS gives you two different ways to run your plugin, both are static builds.
Compile-time Configuration
In this method, you need to clone the CoreDNS source code, add your plugin to the plugin.cfg
file (plugins are ordered), and compile the code.
etcd:etcd
foo:github.com/you/foo
Then you need to do a go get github.com/you/foo
and build the CoreDNS binary using make
.
Run ./coredns -plugins
to ensure your plugin is included in the binary.
If your plugin is on your local machine you can put something like
replace github.com/you/foo => ../foo
in yourgo.mod
file.
Wrapping in External Source Code
You also have the option to wrap the CoreDNS components and your plugin in an external source code and compile from there.
package main import ( _ "github.com/you/foo" "github.com/coredns/coredns/coremain" "github.com/coredns/coredns/core/dnsserver"
) var directives = []string{ "foo", ... ... "whoami", "startup", "shutdown",
} func init() { dnsserver.Directives = directives
} func main() { coremain.Run()
}
As with any other go app, do a go build
and you should have the binary.