WIP: middleware/dnstap (#711)

middleware/dnstap add
This commit is contained in:
varyoo
2017-07-24 23:12:50 +02:00
committed by Miek Gieben
parent f33b02689c
commit 1b7492be6e
41 changed files with 3537 additions and 0 deletions

View File

@@ -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.

View File

@@ -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" }

View File

@@ -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)
}

View File

@@ -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,
}
}

View File

@@ -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}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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())
}

View File

@@ -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)
}
}

View File

@@ -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
}