aboutsummaryrefslogtreecommitdiff
path: root/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'middleware')
-rw-r--r--middleware/dnstap/README.md20
-rw-r--r--middleware/dnstap/handler.go51
-rw-r--r--middleware/dnstap/handler_test.go65
-rw-r--r--middleware/dnstap/msg/msg.go89
-rw-r--r--middleware/dnstap/msg/msg_test.go42
-rw-r--r--middleware/dnstap/msg/wrapper.go25
-rw-r--r--middleware/dnstap/out/socket.go86
-rw-r--r--middleware/dnstap/out/socket_test.go94
-rw-r--r--middleware/dnstap/setup.go70
-rw-r--r--middleware/dnstap/setup_test.go19
-rw-r--r--middleware/dnstap/taprw/writer.go84
-rw-r--r--middleware/dnstap/taprw/writer_test.go96
-rw-r--r--middleware/dnstap/test/helpers.go64
13 files changed, 805 insertions, 0 deletions
diff --git a/middleware/dnstap/README.md b/middleware/dnstap/README.md
new file mode 100644
index 000000000..a9d47d501
--- /dev/null
+++ b/middleware/dnstap/README.md
@@ -0,0 +1,20 @@
+# Dnstap
+
+## Syntax
+
+`dnstap SOCKET [full]`
+
+* **SOCKET** is the socket path supplied to the dnstap command line tool.
+* `full` to include the wire-format dns message.
+
+## Dnstap command line tool
+
+```sh
+go get github.com/dnstap/golang-dnstap
+cd $GOPATH/src/github.com/dnstap/golang-dnstap/dnstap
+go build
+./dnstap -u /tmp/dnstap.sock
+./dnstap -u /tmp/dnstap.sock -y
+```
+
+There is a buffer, expect at least 13 requests before the server sends its dnstap messages to the socket.
diff --git a/middleware/dnstap/handler.go b/middleware/dnstap/handler.go
new file mode 100644
index 000000000..20121ff2e
--- /dev/null
+++ b/middleware/dnstap/handler.go
@@ -0,0 +1,51 @@
+package dnstap
+
+import (
+ "fmt"
+ "golang.org/x/net/context"
+ "io"
+
+ "github.com/coredns/coredns/middleware"
+ "github.com/coredns/coredns/middleware/dnstap/msg"
+ "github.com/coredns/coredns/middleware/dnstap/taprw"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+type Dnstap struct {
+ Next middleware.Handler
+ Out io.Writer
+ Pack bool
+}
+
+func tapMessageTo(w io.Writer, m *tap.Message) error {
+ frame, err := msg.Marshal(m)
+ if err != nil {
+ return fmt.Errorf("marshal: %s", err)
+ }
+ _, err = w.Write(frame)
+ return err
+}
+
+func (h Dnstap) TapMessage(m *tap.Message) error {
+ return tapMessageTo(h.Out, m)
+}
+
+func (h Dnstap) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
+ rw := taprw.ResponseWriter{ResponseWriter: w, Taper: &h, Query: r, Pack: h.Pack}
+ rw.QueryEpoch()
+
+ code, err := middleware.NextOrFailure(h.Name(), h.Next, ctx, &rw, r)
+ if err != nil {
+ // ignore dnstap errors
+ return code, err
+ }
+
+ if err := rw.DnstapError(); err != nil {
+ return code, middleware.Error("dnstap", err)
+ }
+
+ return code, nil
+}
+func (h Dnstap) Name() string { return "dnstap" }
diff --git a/middleware/dnstap/handler_test.go b/middleware/dnstap/handler_test.go
new file mode 100644
index 000000000..dfdde582d
--- /dev/null
+++ b/middleware/dnstap/handler_test.go
@@ -0,0 +1,65 @@
+package dnstap
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "github.com/coredns/coredns/middleware/dnstap/test"
+ mwtest "github.com/coredns/coredns/middleware/test"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/golang/protobuf/proto"
+ "github.com/miekg/dns"
+ "golang.org/x/net/context"
+)
+
+func testCase(t *testing.T, tapq, tapr *tap.Message, q, r *dns.Msg) {
+ w := writer{}
+ w.queue = append(w.queue, tapq, tapr)
+ h := Dnstap{
+ Next: mwtest.HandlerFunc(func(_ context.Context,
+ w dns.ResponseWriter, _ *dns.Msg) (int, error) {
+
+ return 0, w.WriteMsg(r)
+ }),
+ Out: &w,
+ Pack: false,
+ }
+ _, err := h.ServeDNS(context.TODO(), &mwtest.ResponseWriter{}, q)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+type writer struct {
+ queue []*tap.Message
+}
+
+func (w *writer) Write(b []byte) (int, error) {
+ e := tap.Dnstap{}
+ if err := proto.Unmarshal(b, &e); err != nil {
+ return 0, err
+ }
+ if len(w.queue) == 0 {
+ return 0, errors.New("message not expected")
+ }
+ if !test.MsgEqual(w.queue[0], e.Message) {
+ return 0, fmt.Errorf("want: %v, have: %v", w.queue[0], e.Message)
+ }
+ w.queue = w.queue[1:]
+ return len(b), nil
+}
+
+func TestDnstap(t *testing.T) {
+ q := mwtest.Case{Qname: "example.org", Qtype: dns.TypeA}.Msg()
+ r := mwtest.Case{
+ Qname: "example.org.", Qtype: dns.TypeA,
+ Answer: []dns.RR{
+ mwtest.A("example.org. 3600 IN A 10.0.0.1"),
+ },
+ }.Msg()
+ tapq := test.TestingData().ToClientQuery()
+ tapr := test.TestingData().ToClientResponse()
+ testCase(t, tapq, tapr, q, r)
+}
diff --git a/middleware/dnstap/msg/msg.go b/middleware/dnstap/msg/msg.go
new file mode 100644
index 000000000..97c7ec7cc
--- /dev/null
+++ b/middleware/dnstap/msg/msg.go
@@ -0,0 +1,89 @@
+// Package msg helps to build a dnstap Message.
+package msg
+
+import (
+ "errors"
+ "net"
+ "time"
+
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+// Data helps to build a dnstap Message.
+// It can be transformed into the actual Message using this package.
+type Data struct {
+ Type tap.Message_Type
+ Packed []byte
+ SocketProto tap.SocketProtocol
+ SocketFam tap.SocketFamily
+ Address []byte
+ Port uint32
+ TimeSec uint64
+}
+
+func (d *Data) FromRequest(r request.Request) error {
+ switch addr := r.W.RemoteAddr().(type) {
+ case *net.TCPAddr:
+ d.Address = addr.IP
+ d.Port = uint32(addr.Port)
+ d.SocketProto = tap.SocketProtocol_TCP
+ case *net.UDPAddr:
+ d.Address = addr.IP
+ d.Port = uint32(addr.Port)
+ d.SocketProto = tap.SocketProtocol_UDP
+ default:
+ return errors.New("unknown remote address type")
+ }
+
+ if a := net.IP(d.Address); a.To4() != nil {
+ d.SocketFam = tap.SocketFamily_INET
+ } else {
+ d.SocketFam = tap.SocketFamily_INET6
+ }
+
+ return nil
+}
+
+func (d *Data) Pack(m *dns.Msg) error {
+ packed, err := m.Pack()
+ if err != nil {
+ return err
+ }
+ d.Packed = packed
+ return nil
+}
+
+func (d *Data) Epoch() {
+ d.TimeSec = uint64(time.Now().Unix())
+}
+
+// Transform the data into a client response message.
+func (d *Data) ToClientResponse() *tap.Message {
+ d.Type = tap.Message_CLIENT_RESPONSE
+ return &tap.Message{
+ Type: &d.Type,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ ResponseTimeSec: &d.TimeSec,
+ ResponseMessage: d.Packed,
+ QueryAddress: d.Address,
+ QueryPort: &d.Port,
+ }
+}
+
+// Transform the data into a client query message.
+func (d *Data) ToClientQuery() *tap.Message {
+ d.Type = tap.Message_CLIENT_QUERY
+ return &tap.Message{
+ Type: &d.Type,
+ SocketFamily: &d.SocketFam,
+ SocketProtocol: &d.SocketProto,
+ QueryTimeSec: &d.TimeSec,
+ QueryMessage: d.Packed,
+ QueryAddress: d.Address,
+ QueryPort: &d.Port,
+ }
+}
diff --git a/middleware/dnstap/msg/msg_test.go b/middleware/dnstap/msg/msg_test.go
new file mode 100644
index 000000000..6a54a9e8c
--- /dev/null
+++ b/middleware/dnstap/msg/msg_test.go
@@ -0,0 +1,42 @@
+package msg
+
+import (
+ "net"
+ "reflect"
+ "testing"
+
+ "github.com/coredns/coredns/middleware/test"
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+func testRequest(t *testing.T, expected Data, r request.Request) {
+ d := Data{}
+ if err := d.FromRequest(r); err != nil {
+ t.Fail()
+ return
+ }
+ if d.SocketProto != expected.SocketProto ||
+ d.SocketFam != expected.SocketFam ||
+ !reflect.DeepEqual(d.Address, expected.Address) ||
+ d.Port != expected.Port {
+ t.Fatalf("expected: %v, have: %v", expected, d)
+ return
+ }
+}
+func TestRequest(t *testing.T) {
+ testRequest(t, Data{
+ SocketProto: tap.SocketProtocol_UDP,
+ SocketFam: tap.SocketFamily_INET,
+ Address: net.ParseIP("10.240.0.1"),
+ Port: 40212,
+ }, testingRequest())
+}
+func testingRequest() request.Request {
+ m := new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeA)
+ m.SetEdns0(4097, true)
+ return request.Request{W: &test.ResponseWriter{}, Req: m}
+}
diff --git a/middleware/dnstap/msg/wrapper.go b/middleware/dnstap/msg/wrapper.go
new file mode 100644
index 000000000..0cb6a76c0
--- /dev/null
+++ b/middleware/dnstap/msg/wrapper.go
@@ -0,0 +1,25 @@
+package msg
+
+import (
+ "fmt"
+
+ lib "github.com/dnstap/golang-dnstap"
+ "github.com/golang/protobuf/proto"
+)
+
+func wrap(m *lib.Message) *lib.Dnstap {
+ t := lib.Dnstap_MESSAGE
+ return &lib.Dnstap{
+ Type: &t,
+ Message: m,
+ }
+}
+
+func Marshal(m *lib.Message) (data []byte, err error) {
+ data, err = proto.Marshal(wrap(m))
+ if err != nil {
+ err = fmt.Errorf("proto: %s", err)
+ return
+ }
+ return
+}
diff --git a/middleware/dnstap/out/socket.go b/middleware/dnstap/out/socket.go
new file mode 100644
index 000000000..520dcf1d8
--- /dev/null
+++ b/middleware/dnstap/out/socket.go
@@ -0,0 +1,86 @@
+package out
+
+import (
+ "fmt"
+ "net"
+
+ fs "github.com/farsightsec/golang-framestream"
+)
+
+// Socket is a Frame Streams encoder over a UNIX socket.
+type Socket struct {
+ path string
+ enc *fs.Encoder
+ conn net.Conn
+ err error
+}
+
+func openSocket(s *Socket) error {
+ conn, err := net.Dial("unix", s.path)
+ if err != nil {
+ return err
+ }
+ s.conn = conn
+
+ enc, err := fs.NewEncoder(conn, &fs.EncoderOptions{
+ ContentType: []byte("protobuf:dnstap.Dnstap"),
+ Bidirectional: true,
+ })
+ if err != nil {
+ return err
+ }
+ s.enc = enc
+
+ s.err = nil
+ return nil
+}
+
+// NewSocket will always return a new Socket.
+// err if nothing is listening to it, it will attempt to reconnect on the next Write.
+func NewSocket(path string) (s *Socket, err error) {
+ s = &Socket{path: path}
+ if err = openSocket(s); err != nil {
+ err = fmt.Errorf("open socket: %s", err)
+ s.err = err
+ return
+ }
+ return
+}
+
+// Write a single Frame Streams frame.
+func (s *Socket) Write(frame []byte) (int, error) {
+ if s.err != nil {
+ // is the dnstap tool listening?
+ if err := openSocket(s); err != nil {
+ return 0, fmt.Errorf("open socket: %s", err)
+ }
+ }
+ n, err := s.enc.Write(frame)
+ if err != nil {
+ // the dnstap command line tool is down
+ s.conn.Close()
+ s.err = err
+ return 0, err
+ }
+ return n, nil
+
+}
+
+// Close the socket and flush the remaining frames.
+func (s *Socket) Close() error {
+ if s.err != nil {
+ // nothing to close
+ return nil
+ }
+
+ defer s.conn.Close()
+
+ if err := s.enc.Flush(); err != nil {
+ return fmt.Errorf("flush: %s", err)
+ }
+ if err := s.enc.Close(); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/middleware/dnstap/out/socket_test.go b/middleware/dnstap/out/socket_test.go
new file mode 100644
index 000000000..050a38d36
--- /dev/null
+++ b/middleware/dnstap/out/socket_test.go
@@ -0,0 +1,94 @@
+package out
+
+import (
+ "net"
+ "testing"
+
+ fs "github.com/farsightsec/golang-framestream"
+)
+
+func acceptOne(t *testing.T, l net.Listener) {
+ server, err := l.Accept()
+ if err != nil {
+ t.Fatalf("server accept: %s", err)
+ return
+ }
+
+ dec, err := fs.NewDecoder(server, &fs.DecoderOptions{
+ ContentType: []byte("protobuf:dnstap.Dnstap"),
+ Bidirectional: true,
+ })
+ if err != nil {
+ t.Fatalf("server decoder: %s", err)
+ return
+ }
+
+ if _, err := dec.Decode(); err != nil {
+ t.Errorf("server decode: %s", err)
+ }
+
+ if err := server.Close(); err != nil {
+ t.Error(err)
+ }
+}
+func sendOne(socket *Socket) error {
+ if _, err := socket.Write([]byte("frame")); err != nil {
+ return err
+ }
+ if err := socket.enc.Flush(); err != nil {
+ // Would happen during Write in real life.
+ socket.conn.Close()
+ socket.err = err
+ return err
+ }
+ return nil
+}
+func TestSocket(t *testing.T) {
+ socket, err := NewSocket("dnstap.sock")
+ if err == nil {
+ t.Fatal("new socket: not listening but no error")
+ return
+ }
+
+ if err := sendOne(socket); err == nil {
+ t.Fatal("not listening but no error")
+ return
+ }
+
+ l, err := net.Listen("unix", "dnstap.sock")
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+
+ wait := make(chan bool)
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOne(socket); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+ if err := sendOne(socket); err == nil {
+ panic("must fail")
+ }
+
+ go func() {
+ acceptOne(t, l)
+ wait <- true
+ }()
+
+ if err := sendOne(socket); err != nil {
+ t.Fatalf("send one: %s", err)
+ return
+ }
+
+ <-wait
+ if err := l.Close(); err != nil {
+ t.Error(err)
+ }
+}
diff --git a/middleware/dnstap/setup.go b/middleware/dnstap/setup.go
new file mode 100644
index 000000000..5c74bd346
--- /dev/null
+++ b/middleware/dnstap/setup.go
@@ -0,0 +1,70 @@
+package dnstap
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/coredns/coredns/core/dnsserver"
+ "github.com/coredns/coredns/middleware"
+ "github.com/coredns/coredns/middleware/dnstap/out"
+
+ "github.com/mholt/caddy"
+ "github.com/mholt/caddy/caddyfile"
+)
+
+func init() {
+ caddy.RegisterPlugin("dnstap", caddy.Plugin{
+ ServerType: "dns",
+ Action: wrapSetup,
+ })
+}
+
+func wrapSetup(c *caddy.Controller) error {
+ if err := setup(c); err != nil {
+ return middleware.Error("dnstap", err)
+ }
+ return nil
+}
+
+func parseConfig(c *caddyfile.Dispenser) (path string, full bool, err error) {
+ c.Next() // directive name
+
+ if !c.Args(&path) {
+ err = c.ArgErr()
+ return
+ }
+
+ full = c.NextArg() && c.Val() == "full"
+
+ return
+}
+
+func setup(c *caddy.Controller) error {
+ path, full, err := parseConfig(&c.Dispenser)
+ if err != nil {
+ return err
+ }
+
+ dnstap := Dnstap{Pack: full}
+
+ o, err := out.NewSocket(path)
+ if err != nil {
+ log.Printf("[WARN] Can't connect to %s at the moment", path)
+ }
+ dnstap.Out = o
+
+ c.OnShutdown(func() error {
+ if err := o.Close(); err != nil {
+ return fmt.Errorf("output: %s", err)
+ }
+ return nil
+ })
+
+ dnsserver.GetConfig(c).AddMiddleware(
+ func(next middleware.Handler) middleware.Handler {
+ dnstap.Next = next
+ return dnstap
+ })
+
+ return nil
+}
diff --git a/middleware/dnstap/setup_test.go b/middleware/dnstap/setup_test.go
new file mode 100644
index 000000000..fb4bca1f0
--- /dev/null
+++ b/middleware/dnstap/setup_test.go
@@ -0,0 +1,19 @@
+package dnstap
+
+import (
+ "github.com/mholt/caddy"
+ "testing"
+)
+
+func TestConfig(t *testing.T) {
+ file := "dnstap dnstap.sock full"
+ c := caddy.NewTestController("dns", file)
+ if path, full, err := parseConfig(&c.Dispenser); path != "dnstap.sock" || !full {
+ t.Fatalf("%s: %s", file, err)
+ }
+ file = "dnstap dnstap.sock"
+ c = caddy.NewTestController("dns", file)
+ if path, full, err := parseConfig(&c.Dispenser); path != "dnstap.sock" || full {
+ t.Fatalf("%s: %s", file, err)
+ }
+}
diff --git a/middleware/dnstap/taprw/writer.go b/middleware/dnstap/taprw/writer.go
new file mode 100644
index 000000000..96f560139
--- /dev/null
+++ b/middleware/dnstap/taprw/writer.go
@@ -0,0 +1,84 @@
+// Package taprw takes a query and intercepts the response.
+// It will log both after the response is written.
+package taprw
+
+import (
+ "fmt"
+
+ "github.com/coredns/coredns/middleware/dnstap/msg"
+ "github.com/coredns/coredns/request"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+type Taper interface {
+ TapMessage(m *tap.Message) error
+}
+
+// Single request use.
+type ResponseWriter struct {
+ queryData msg.Data
+ Query *dns.Msg
+ dns.ResponseWriter
+ Taper
+ Pack bool
+ err error
+}
+
+// Check if a dnstap error occured.
+// Set during ResponseWriter.Write.
+func (w ResponseWriter) DnstapError() error {
+ return w.err
+}
+
+// To be called as soon as possible.
+func (w *ResponseWriter) QueryEpoch() {
+ w.queryData.Epoch()
+}
+
+// Write back the response to the client and THEN work on logging the request
+// and response to dnstap.
+// Dnstap errors to be checked by DnstapError.
+func (w *ResponseWriter) WriteMsg(resp *dns.Msg) error {
+ writeErr := w.ResponseWriter.WriteMsg(resp)
+
+ if err := tapQuery(w); err != nil {
+ w.err = fmt.Errorf("client query: %s", err)
+ // don't forget to call DnstapError later
+ }
+
+ if writeErr == nil {
+ if err := tapResponse(w, resp); err != nil {
+ w.err = fmt.Errorf("client response: %s", err)
+ }
+ }
+
+ return writeErr
+}
+func tapQuery(w *ResponseWriter) error {
+ req := request.Request{W: w.ResponseWriter, Req: w.Query}
+ if err := w.queryData.FromRequest(req); err != nil {
+ return err
+ }
+ if w.Pack {
+ if err := w.queryData.Pack(w.Query); err != nil {
+ return fmt.Errorf("pack: %s", err)
+ }
+ }
+ return w.Taper.TapMessage(w.queryData.ToClientQuery())
+}
+func tapResponse(w *ResponseWriter, resp *dns.Msg) error {
+ d := &msg.Data{}
+ d.Epoch()
+ req := request.Request{W: w, Req: resp}
+ if err := d.FromRequest(req); err != nil {
+ return err
+ }
+ if w.Pack {
+ if err := d.Pack(resp); err != nil {
+ return fmt.Errorf("pack: %s", err)
+ }
+ }
+ return w.Taper.TapMessage(d.ToClientResponse())
+}
diff --git a/middleware/dnstap/taprw/writer_test.go b/middleware/dnstap/taprw/writer_test.go
new file mode 100644
index 000000000..a19271e7c
--- /dev/null
+++ b/middleware/dnstap/taprw/writer_test.go
@@ -0,0 +1,96 @@
+package taprw
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/coredns/coredns/middleware/dnstap/test"
+ mwtest "github.com/coredns/coredns/middleware/test"
+
+ tap "github.com/dnstap/golang-dnstap"
+ "github.com/miekg/dns"
+)
+
+type TapFailer struct {
+}
+
+func (TapFailer) TapMessage(*tap.Message) error {
+ return errors.New("failed")
+}
+
+func TestDnstapError(t *testing.T) {
+ rw := ResponseWriter{
+ Query: new(dns.Msg),
+ ResponseWriter: &mwtest.ResponseWriter{},
+ Taper: TapFailer{},
+ }
+ if err := rw.WriteMsg(new(dns.Msg)); err != nil {
+ t.Errorf("dnstap error during Write: %s", err)
+ }
+ if rw.DnstapError() == nil {
+ t.Fatal("no dnstap error")
+ }
+}
+
+func testingMsg() (m *dns.Msg) {
+ m = new(dns.Msg)
+ m.SetQuestion("example.com.", dns.TypeA)
+ m.SetEdns0(4097, true)
+ return
+}
+
+func TestClientResponse(t *testing.T) {
+ trapper := test.TrapTaper{}
+ rw := ResponseWriter{
+ Pack: true,
+ Taper: &trapper,
+ ResponseWriter: &mwtest.ResponseWriter{},
+ }
+ d := test.TestingData()
+ m := testingMsg()
+
+ // will the wire-format msg be reported?
+ bin, err := m.Pack()
+ if err != nil {
+ t.Fatal(err)
+ return
+ }
+ d.Packed = bin
+
+ if err := tapResponse(&rw, m); err != nil {
+ t.Fatal(err)
+ return
+ }
+ want := d.ToClientResponse()
+ if l := len(trapper.Trap); l != 1 {
+ t.Fatalf("%d msg trapped", l)
+ return
+ }
+ have := trapper.Trap[0]
+ if !test.MsgEqual(want, have) {
+ t.Fatalf("want: %v\nhave: %v", want, have)
+ }
+}
+
+func TestClientQuery(t *testing.T) {
+ trapper := test.TrapTaper{}
+ rw := ResponseWriter{
+ Pack: false, // no binary this time
+ Taper: &trapper,
+ ResponseWriter: &mwtest.ResponseWriter{},
+ Query: testingMsg(),
+ }
+ if err := tapQuery(&rw); err != nil {
+ t.Fatal(err)
+ return
+ }
+ want := test.TestingData().ToClientQuery()
+ if l := len(trapper.Trap); l != 1 {
+ t.Fatalf("%d msg trapped", l)
+ return
+ }
+ have := trapper.Trap[0]
+ if !test.MsgEqual(want, have) {
+ t.Fatalf("want: %v\nhave: %v", want, have)
+ }
+}
diff --git a/middleware/dnstap/test/helpers.go b/middleware/dnstap/test/helpers.go
new file mode 100644
index 000000000..fba291dfe
--- /dev/null
+++ b/middleware/dnstap/test/helpers.go
@@ -0,0 +1,64 @@
+package test
+
+import (
+ "net"
+ "reflect"
+
+ "github.com/coredns/coredns/middleware/dnstap/msg"
+
+ tap "github.com/dnstap/golang-dnstap"
+)
+
+func TestingData() (d *msg.Data) {
+ d = &msg.Data{
+ Type: tap.Message_CLIENT_RESPONSE,
+ SocketFam: tap.SocketFamily_INET,
+ SocketProto: tap.SocketProtocol_UDP,
+ Address: net.ParseIP("10.240.0.1"),
+ Port: 40212,
+ }
+ return
+}
+
+type comp struct {
+ Type *tap.Message_Type
+ SF *tap.SocketFamily
+ SP *tap.SocketProtocol
+ QA []byte
+ RA []byte
+ QP *uint32
+ RP *uint32
+ QTSec bool
+ RTSec bool
+ RM []byte
+ QM []byte
+}
+
+func toComp(m *tap.Message) comp {
+ return comp{
+ Type: m.Type,
+ SF: m.SocketFamily,
+ SP: m.SocketProtocol,
+ QA: m.QueryAddress,
+ RA: m.ResponseAddress,
+ QP: m.QueryPort,
+ RP: m.ResponsePort,
+ QTSec: m.QueryTimeSec != nil,
+ RTSec: m.ResponseTimeSec != nil,
+ RM: m.ResponseMessage,
+ QM: m.QueryMessage,
+ }
+}
+
+func MsgEqual(a, b *tap.Message) bool {
+ return reflect.DeepEqual(toComp(a), toComp(b))
+}
+
+type TrapTaper struct {
+ Trap []*tap.Message
+}
+
+func (t *TrapTaper) TapMessage(m *tap.Message) error {
+ t.Trap = append(t.Trap, m)
+ return nil
+}