diff options
Diffstat (limited to 'middleware')
-rw-r--r-- | middleware/dnstap/README.md | 20 | ||||
-rw-r--r-- | middleware/dnstap/handler.go | 51 | ||||
-rw-r--r-- | middleware/dnstap/handler_test.go | 65 | ||||
-rw-r--r-- | middleware/dnstap/msg/msg.go | 89 | ||||
-rw-r--r-- | middleware/dnstap/msg/msg_test.go | 42 | ||||
-rw-r--r-- | middleware/dnstap/msg/wrapper.go | 25 | ||||
-rw-r--r-- | middleware/dnstap/out/socket.go | 86 | ||||
-rw-r--r-- | middleware/dnstap/out/socket_test.go | 94 | ||||
-rw-r--r-- | middleware/dnstap/setup.go | 70 | ||||
-rw-r--r-- | middleware/dnstap/setup_test.go | 19 | ||||
-rw-r--r-- | middleware/dnstap/taprw/writer.go | 84 | ||||
-rw-r--r-- | middleware/dnstap/taprw/writer_test.go | 96 | ||||
-rw-r--r-- | middleware/dnstap/test/helpers.go | 64 |
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 +} |