CoreDNS is a flexible, extensible DNS server written in Go. While it comes with a rich set of built-in plugins for common DNS tasks, you’ll sometimes need to implement custom logic that isn’t covered by the existing plugins. This is where building a custom CoreDNS plugin shines.
Let’s see a custom plugin in action. Imagine we want a plugin that intercepts DNS requests for a specific domain, say internal.example.com, and always resolves them to a fixed IP address, 192.168.1.100, regardless of what the actual DNS records might say. This is useful for local development or testing.
Here’s a minimal CoreDNS setup using this hypothetical internalresolver plugin:
.:53 {
root
internalresolver internal.example.com 192.168.1.100
forward . 8.8.8.8
}
When a client queries for host1.internal.example.com, the internalresolver plugin will match and return 192.168.1.100. If the query is for google.com, it will pass through to the forward plugin.
To build this, we’ll create a Go plugin. The core of a CoreDNS plugin is the ServeDNS function, which takes a context.Context and a dns.Msg (the DNS request) and returns a dns.Msg (the DNS response) and an error.
package internalresolver
import (
"context"
"fmt"
"net"
"strings"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
)
// Handler implements the DNS handler.
type Handler struct {
Next plugin.Handler
Domain string
TargetIP net.IP
DomainPath string // The full path of the plugin in the Corefile
}
// ServeDNS implements the plugin.Handler interface.
func (h *Handler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
msg := new(dns.Msg)
msg.SetReply(r)
msg.Authoritative = true
// Only process queries for the specific domain
qname := strings.ToLower(r.Question[0].Name)
if !strings.HasSuffix(qname, h.Domain) {
return h.Next.ServeDNS(ctx, w, r)
}
// Log the request for debugging
fmt.Printf("Intercepted request for %s, resolving to %s\n", qname, h.TargetIP.String())
// Create an A record for the response
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.Ns = nil // No authority section for this simple override
m.Extra = nil // No extra records
rr, err := dns.NewRR(fmt.Sprintf("%s A %s", qname, h.TargetIP.String()))
if err != nil {
return dns.RcodeServerFailure, fmt.Errorf("failed to create RR: %v", err)
}
m.Answer = append(m.Answer, rr)
// Write the response back to the client
w.WriteMsg(m)
return dns.RcodeSuccess, nil
}
// Name returns the name of the plugin.
func (h *Handler) Name() string { return "internalresolver" }
The internalresolver plugin will be configured in the Corefile with the domain to intercept and the IP address to return. The ServeDNS function first checks if the query name ends with the configured domain. If it does, it constructs a DNS response with an A record pointing to the target IP. If not, it passes the request to the next plugin in the chain using h.Next.ServeDNS.
To make this plugin loadable by CoreDNS, you need to register it. This is typically done in a plugin.go file within your plugin’s directory.
package internalresolver
import (
"fmt"
"net"
"strings"
"github.com/coredns/coredns/coremain"
"github.com/coredns/coredns/plugin"
)
func init() {
// Register the plugin with a name and a setup function
plugin.Register("internalresolver", setup)
}
// setup is the function that CoreDNS calls to set up the plugin.
func setup(c *plugin.Args) error {
// Parse the configuration from the Corefile
// Example: internalresolver internal.example.com 192.168.1.100
if !c.NextArg() {
return plugin.Error("internalresolver", fmt.Errorf("missing domain and IP address"))
}
domain := strings.ToLower(c.Arg())
if !c.NextArg() {
return plugin.Error("internalresolver", fmt.Errorf("missing IP address for domain %s", domain))
}
ipStr := c.Arg()
ip := net.ParseIP(ipStr)
if ip == nil {
return plugin.Error("internalresolver", fmt.Errorf("invalid IP address %s", ipStr))
}
// Create a new instance of our plugin handler
h := &Handler{
Domain: "." + domain, // Ensure it's a fully qualified domain name for suffix matching
TargetIP: ip,
DomainPath: plugin.FilePath(c.Path()), // Store the path for logging/debugging
}
// Add the handler to the plugin chain
c.AddPlugin(func(next plugin.Handler) plugin.Handler {
h.Next = next
return h
})
return nil
}
The setup function is crucial. It’s called by CoreDNS when it parses the Corefile. It parses the arguments provided to the plugin (internal.example.com and 192.168.1.100 in our example), validates them, creates an instance of our Handler, and then tells CoreDNS how to insert this handler into the plugin chain. The plugin.FilePath(c.Path()) is a convention to capture the location in the Corefile where the plugin was declared.
One subtle point is how the domain matching works. strings.HasSuffix(qname, h.Domain) is used. We prepend a dot to the configured domain ("." + domain) in setup to ensure that internal.example.com correctly matches host1.internal.example.com but not another-internal.example.com. Without the leading dot, internal.example.com would also be a suffix of another-internal.example.com, which is usually not desired.
The plugin.Register call in init() makes our plugin discoverable by CoreDNS.
Building and running a custom plugin involves compiling your Go code into a shared library that CoreDNS can load. CoreDNS uses a plugin architecture where plugins are dynamically loaded. You’ll typically compile your plugin, then place the shared object file (.so) in a directory that CoreDNS is configured to look for plugins, and then reference it in your Corefile.
The next step after successfully implementing custom resolution logic is to explore how to handle different DNS record types beyond A records, such as CNAME, MX, or TXT records, within your plugin.