Development of a Handler

Developing a new handler requires the source code of atomdns to be checked out, see installation for instructions. It’s assume you are in the cmd/atomdns/ subdirectory.

In this tutorial we are going to implement a handler called foo.

Registration

A new handler must be “registered” in atomdns. This process has been automated and only requires a go generate command, provided the following is done.

The name of our handler is “foo”, so the name of the main handler file must be foo.go and must contain a Foo type. The Foo type must implement the handlers.Handler interface.

So: mkdir foo, and then $EDITOR handlers/foo/foo.go:

The contents of this file becomes:

package foo

import (
	"context"

	"codeberg.org/miekg/dns"
)

type Foo int

func (f *Foo) HandlerFunc(next dns.HandlerFunc) dns.HandlerFunc {
	return dns.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
	})
}

Readme

Any handler needs documentation, and foo is not an exception. The documentation style is heavily influenced by Unix manual pages. So create a handlers/foo/README.md with the following contents:

# Name

_foo_ - return 198.51.100.1 for every query

# Description

_foo_ returns the same IP address for all queries.

# Syntax

`foo`

Once both are saved, run:

go generate ./...

… to generate a handlers/foo/zerr.go file that has some bits atomdns uses. foo now is a full fledged handler, but it doesn’t do much. This also generates a man/atomdns-foo.7 manual page.

If you compile atomdns (go build) and run it, you’ll see foo is listed as a handler:

% ./atomdns -H|grep foo
foo

At this point we assume this handler does not need any setup, so we defer implementing a setup function, instead we flesh out the HandlerFunc method.

HandlerFunc

We’ll create a HandlerFunc that returns a single A record for every query. The address returned is a document IP address from RFC 5737.

We create an answer message from the reply - this way we can re-use the buffer.

m := r.Copy()
dnsutil.SetReply(m, r)

dnsutil.SetReply is documented locally, but also in go.dev.

Next we create our A record:

m.Answer = []dns.RR{
	&dns.A{
		Hdr: dns.Header{Name: dns.Zone(ctx), Class: dns.ClassINET, TTL: 3600},
		A:   dnstest.IPv4,
	},
}

This utilizes dns.Zone to get the DNS zone that atomdns has linked to our foo handler, i.e.

example.org {
    foo
}

Would make dns.Zone return “example.org.”.

Lastly we want to perform any registered (by other handlers) modification functions and then io.Copy the reply back to the client.

m = dnsctx.Funcs(ctx, m)
if err := m.Pack(); err != nil {
	log().Debug("Pack failure", Err(err))
}
io.Copy(w, m)

This make the full code of handlers/foo/foo.go:

package foo

import (
	"context"
	"io"

	"codeberg.org/miekg/dns"
	"codeberg.org/miekg/dns/cmd/atomdns/internal/dnsctx"
	"codeberg.org/miekg/dns/dnstest"
	"codeberg.org/miekg/dns/dnsutil"
)

type Foo int

func (f *Foo) HandlerFunc(next dns.HandlerFunc) dns.HandlerFunc {
	return dns.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
		m := r.Copy()
		dnsutil.SetReply(m, r)
		m.Answer = []dns.RR{
			&dns.A{
				Hdr: dns.Header{Name: dns.Zone(ctx), Class: dns.ClassINET, TTL: 3600},
				A:   dnstest.IPv4,
			},
		}

		m = dnsctx.Funcs(ctx, m)
		if err := m.Pack(); err != nil {
			log().Debug("Pack failure", Err(err))
		}
		io.Copy(w, m)
	})

Testing

We can already this what we have now, create a Conffile with this contents:

{
    dns {
        addr [::]:1053
    }
}

example.org {
    log
    foo
}

Then run ./atomdns -c Conffile and query the example.org zone: dig @localhost +noall +answer -p 1053 www.example.org, this should display: example.org. 3600 IN A 198.51.100.1.

Setup

Let’s say we want to make the returned IP address configurable, in the Conffile with something like:

foo [IP]

Where the IP is an optional argument to the foo handler.

For this foo needs to implement the handler.Setupper interface. The Foo type needs to be slightly changed from type Foo int to

type Foo struct {
    IP string
}

Then we create handlers/foo/setup.go, with the following in it:

package foo

import (
	"codeberg.org/miekg/dns/cmd/atomdns/internal/dnsserver"
)

func (f *Foo) Setup(co *dnsserver.Controller) error {
	for co.Next() {
		args, err := co.RemainingIPs()
		if err != nil {
			return err
		}
		if len(args) > 0 {
			f.IP = args[0]
		}
	}
    if f.IP == ""  {
        f.IP = "198.51.100.1"
    }
	return nil
}

Here we utilize reading from *dnsserver.Controller to get the token from the configuration file. We check if we got an IP address (co.RemainingIPs()) and if so set to f’s IP field. If none are set we set the default address that we used before.

Just by the virtue of having Setup method it will be used by atomdns.

Our HandleFunc does not use this information yet. This difference with the previous iteration is:

diff --git i/cmd/atomdns/handlers/foo/foo.go w/cmd/atomdns/handlers/foo/foo.go
index 886700d..2591e43 100644
--- i/cmd/atomdns/handlers/foo/foo.go
+++ w/cmd/atomdns/handlers/foo/foo.go
@@ -3,14 +3,16 @@ package foo
 import (
        "context"
        "io"
+       "net"

        "codeberg.org/miekg/dns"
        "codeberg.org/miekg/dns/cmd/atomdns/internal/dnsctx"
-       "codeberg.org/miekg/dns/dnstest"
        "codeberg.org/miekg/dns/dnsutil"
 )

-type Foo int
+type Foo struct {
+       IP string
+}

 func (f *Foo) HandlerFunc(next dns.HandlerFunc) dns.HandlerFunc {
        return dns.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) {
@@ -19,7 +21,7 @@ func (f *Foo) HandlerFunc(next dns.HandlerFunc) dns.HandlerFunc {
                m.Answer = []dns.RR{
                        &dns.A{
                                Hdr: dns.Header{Name: dns.Zone(ctx), Class: dns.ClassINET, TTL: 3600},
-                               A:   dnstest.IPv4,
+                               A:   net.ParseIP(f.IP),
                        },
                }

With a slightly amended config file we can return any IP we want:

{
    dns {
        addr [::]:1053
    }
}

example.org {
    foo 127.0.0.1
}

Startup and Shutdown Functions

If for some reason start up function and tear down functions need to be run you can use

  • co.OnStartup to run on startup. It is customary to announce your existence:
    co.OnStartup(func() error {
      log().Info("Startup", "hello", "from me")
    })
  • co.OnShutdown to run on at shutdown. It is customary to announce your existence:
    co.OnShutdown(func() error {
      log().Info("Shutdown", "goodbye", "from me")
    })

These need to be added to handlers/foo/setup.go. Note that on reload these functions are run.