DEV Community

Amir Keshavarz
Amir Keshavarz

Posted on

Developing Custom Plugins for CoreDNS

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.

CoreDNS Flow

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 and debug 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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  1. Skip the first token which is foo, the name of our argument.
  2. Return an error if our argument didn't have any value.
  3. 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.
  4. 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
}
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode

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)

}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 or udp
  • state.Family() IP version - 1 for IPv4 and 2 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
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 your go.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()
}
Enter fullscreen mode Exit fullscreen mode

As with any other go app, do a go build and you should have the binary.

Links

Top comments (0)