middleware/proxy: dnstap (#786)

* experimental dnstap support into proxy

* proxy reports dnstap errors

* refactoring

* add a message builder for less dnstap code

* msg lint

* context

* proxy by DNS: dnstap comments

* TapBuilder

* resolves conflict

* dnstap into ServeDNS

* testing

* more tests

* `go lint`

* doc update
This commit is contained in:
varyoo
2017-09-01 12:41:41 +02:00
committed by Miek Gieben
parent 8f77566cdd
commit c5efd45720
13 changed files with 317 additions and 97 deletions

View File

@@ -4,22 +4,40 @@ import (
"fmt"
"io"
"golang.org/x/net/context"
"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"
"golang.org/x/net/context"
)
// Dnstap is the dnstap handler.
type Dnstap struct {
Next middleware.Handler
Out io.Writer
Pack bool
}
type (
// Tapper is implemented by the Context passed by the dnstap handler.
Tapper interface {
TapMessage(*tap.Message) error
TapBuilder() msg.Builder
}
tapContext struct {
context.Context
Dnstap
}
)
// TapperFromContext will return a Tapper if the dnstap middleware is enabled.
func TapperFromContext(ctx context.Context) (t Tapper) {
t, _ = ctx.(Tapper)
return
}
func tapMessageTo(w io.Writer, m *tap.Message) error {
frame, err := msg.Marshal(m)
if err != nil {
@@ -29,15 +47,22 @@ func tapMessageTo(w io.Writer, m *tap.Message) error {
return err
}
// TapMessage implements Tapper.
func (h Dnstap) TapMessage(m *tap.Message) error {
return tapMessageTo(h.Out, m)
}
// TapBuilder implements Tapper.
func (h Dnstap) TapBuilder() msg.Builder {
return msg.Builder{Full: h.Pack}
}
// ServeDNS logs the client query and response to dnstap and passes the dnstap Context.
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 := &taprw.ResponseWriter{ResponseWriter: w, Tapper: &h, Query: r}
rw.QueryEpoch()
code, err := middleware.NextOrFailure(h.Name(), h.Next, ctx, rw, r)
code, err := middleware.NextOrFailure(h.Name(), h.Next, tapContext{ctx, h}, rw, r)
if err != nil {
// ignore dnstap errors
return code, err
@@ -49,4 +74,6 @@ func (h Dnstap) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return code, nil
}
// Name returns dnstap.
func (h Dnstap) Name() string { return "dnstap" }

View File

@@ -4,18 +4,39 @@ package msg
import (
"errors"
"net"
"strconv"
"time"
"github.com/coredns/coredns/request"
tap "github.com/dnstap/golang-dnstap"
"github.com/miekg/dns"
)
// Builder helps to build Data by being aware of the dnstap middleware configuration.
type Builder struct {
Full bool
Data
}
// AddrMsg parses the info of net.Addr and dns.Msg.
func (b *Builder) AddrMsg(a net.Addr, m *dns.Msg) (err error) {
err = b.RemoteAddr(a)
if err != nil {
return
}
return b.Msg(m)
}
// Msg parses the info of dns.Msg.
func (b *Builder) Msg(m *dns.Msg) (err error) {
if b.Full {
err = b.Pack(m)
}
return
}
// 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
@@ -24,8 +45,34 @@ type Data struct {
TimeSec uint64
}
func (d *Data) FromRequest(r request.Request) error {
switch addr := r.W.RemoteAddr().(type) {
// HostPort decodes into Data any string returned by dnsutil.ParseHostPortOrFile.
func (d *Data) HostPort(addr string) error {
ip, port, err := net.SplitHostPort(addr)
if err != nil {
return err
}
p, err := strconv.ParseUint(port, 10, 32)
if err != nil {
return err
}
d.Port = uint32(p)
if ip := net.ParseIP(ip); ip != nil {
d.Address = []byte(ip)
if ip := ip.To4(); ip != nil {
d.SocketFam = tap.SocketFamily_INET
} else {
d.SocketFam = tap.SocketFamily_INET6
}
return nil
} else {
return errors.New("not an ip address")
}
}
// RemoteAddr parses the information about the remote address into Data.
func (d *Data) RemoteAddr(remote net.Addr) error {
switch addr := remote.(type) {
case *net.TCPAddr:
d.Address = addr.IP
d.Port = uint32(addr.Port)
@@ -47,6 +94,7 @@ func (d *Data) FromRequest(r request.Request) error {
return nil
}
// Pack encodes the DNS message into Data.
func (d *Data) Pack(m *dns.Msg) error {
packed, err := m.Pack()
if err != nil {
@@ -56,15 +104,21 @@ func (d *Data) Pack(m *dns.Msg) error {
return nil
}
func (d *Data) Epoch() {
d.TimeSec = uint64(time.Now().Unix())
// Epoch returns the epoch time in seconds.
func Epoch() uint64 {
return uint64(time.Now().Unix())
}
// Transform the data into a client response message.
// Epoch sets the dnstap message epoch.
func (d *Data) Epoch() {
d.TimeSec = Epoch()
}
// ToClientResponse transforms Data into a client response message.
func (d *Data) ToClientResponse() *tap.Message {
d.Type = tap.Message_CLIENT_RESPONSE
t := tap.Message_CLIENT_RESPONSE
return &tap.Message{
Type: &d.Type,
Type: &t,
SocketFamily: &d.SocketFam,
SocketProtocol: &d.SocketProto,
ResponseTimeSec: &d.TimeSec,
@@ -74,11 +128,11 @@ func (d *Data) ToClientResponse() *tap.Message {
}
}
// Transform the data into a client query message.
// ToClientQuery transforms Data into a client query message.
func (d *Data) ToClientQuery() *tap.Message {
d.Type = tap.Message_CLIENT_QUERY
t := tap.Message_CLIENT_QUERY
return &tap.Message{
Type: &d.Type,
Type: &t,
SocketFamily: &d.SocketFam,
SocketProtocol: &d.SocketProto,
QueryTimeSec: &d.TimeSec,
@@ -87,3 +141,29 @@ func (d *Data) ToClientQuery() *tap.Message {
QueryPort: &d.Port,
}
}
// ToOutsideQuery transforms the data into a forwarder or resolver query message.
func (d *Data) ToOutsideQuery(t tap.Message_Type) *tap.Message {
return &tap.Message{
Type: &t,
SocketFamily: &d.SocketFam,
SocketProtocol: &d.SocketProto,
QueryTimeSec: &d.TimeSec,
QueryMessage: d.Packed,
ResponseAddress: d.Address,
ResponsePort: &d.Port,
}
}
// ToOutsideResponse transforms the data into a forwarder or resolver response message.
func (d *Data) ToOutsideResponse(t tap.Message_Type) *tap.Message {
return &tap.Message{
Type: &t,
SocketFamily: &d.SocketFam,
SocketProtocol: &d.SocketProto,
ResponseTimeSec: &d.TimeSec,
ResponseMessage: d.Packed,
ResponseAddress: d.Address,
ResponsePort: &d.Port,
}
}

View File

@@ -14,7 +14,7 @@ import (
func testRequest(t *testing.T, expected Data, r request.Request) {
d := Data{}
if err := d.FromRequest(r); err != nil {
if err := d.RemoteAddr(r.W.RemoteAddr()); err != nil {
t.Fail()
return
}

View File

@@ -15,6 +15,7 @@ func wrap(m *lib.Message) *lib.Dnstap {
}
}
// Marshal encodes the message to a binary dnstap payload.
func Marshal(m *lib.Message) (data []byte, err error) {
data, err = proto.Marshal(wrap(m))
if err != nil {

View File

@@ -6,79 +6,68 @@ 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 {
// Tapper is what ResponseWriter needs to log to dnstap.
type Tapper interface {
TapMessage(m *tap.Message) error
TapBuilder() msg.Builder
}
// ResponseWriter captures the client response and logs the query to dnstap.
// Single request use.
type ResponseWriter struct {
queryData msg.Data
Query *dns.Msg
queryEpoch uint64
Query *dns.Msg
dns.ResponseWriter
Taper
Pack bool
err error
Tapper
err error
}
// Check if a dnstap error occurred.
// Set during ResponseWriter.Write.
// DnstapError check if a dnstap error occurred during Write and returns it.
func (w ResponseWriter) DnstapError() error {
return w.err
}
// To be called as soon as possible.
// QueryEpoch sets the query epoch as reported by dnstap.
func (w *ResponseWriter) QueryEpoch() {
w.queryData.Epoch()
w.queryEpoch = msg.Epoch()
}
// Write back the response to the client and THEN work on logging the request
// WriteMsg writes back the response to the client and THEN works 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)
// Dnstap errors are to be checked by DnstapError.
func (w *ResponseWriter) WriteMsg(resp *dns.Msg) (writeErr error) {
writeErr = w.ResponseWriter.WriteMsg(resp)
writeEpoch := msg.Epoch()
if err := tapQuery(w); err != nil {
b := w.TapBuilder()
b.TimeSec = w.queryEpoch
if err := func() (err error) {
err = b.AddrMsg(w.ResponseWriter.RemoteAddr(), w.Query)
if err != nil {
return
}
return w.TapMessage(b.ToClientQuery())
}(); 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 {
if err := func() (err error) {
b.TimeSec = writeEpoch
if err = b.Msg(resp); err != nil {
return
}
return w.TapMessage(b.ToClientResponse())
}(); 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())
return
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"testing"
"github.com/coredns/coredns/middleware/dnstap/msg"
"github.com/coredns/coredns/middleware/dnstap/test"
mwtest "github.com/coredns/coredns/middleware/test"
@@ -17,12 +18,15 @@ type TapFailer struct {
func (TapFailer) TapMessage(*tap.Message) error {
return errors.New("failed")
}
func (TapFailer) TapBuilder() msg.Builder {
return msg.Builder{Full: true}
}
func TestDnstapError(t *testing.T) {
rw := ResponseWriter{
Query: new(dns.Msg),
ResponseWriter: &mwtest.ResponseWriter{},
Taper: TapFailer{},
Tapper: TapFailer{},
}
if err := rw.WriteMsg(new(dns.Msg)); err != nil {
t.Errorf("dnstap error during Write: %s", err)
@@ -39,15 +43,15 @@ func testingMsg() (m *dns.Msg) {
return
}
func TestClientResponse(t *testing.T) {
trapper := test.TrapTaper{}
func TestClientQueryResponse(t *testing.T) {
trapper := test.TrapTapper{Full: true}
m := testingMsg()
rw := ResponseWriter{
Pack: true,
Taper: &trapper,
Query: m,
Tapper: &trapper,
ResponseWriter: &mwtest.ResponseWriter{},
}
d := test.TestingData()
m := testingMsg()
// will the wire-format msg be reported?
bin, err := m.Pack()
@@ -57,40 +61,22 @@ func TestClientResponse(t *testing.T) {
}
d.Packed = bin
if err := tapResponse(&rw, m); err != nil {
if err := rw.WriteMsg(m); err != nil {
t.Fatal(err)
return
}
want := d.ToClientResponse()
if l := len(trapper.Trap); l != 1 {
if l := len(trapper.Trap); l != 2 {
t.Fatalf("%d msg trapped", l)
return
}
want := d.ToClientQuery()
have := trapper.Trap[0]
if !test.MsgEqual(want, have) {
t.Fatalf("want: %v\nhave: %v", want, have)
t.Fatalf("query: 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]
want = d.ToClientResponse()
have = trapper.Trap[1]
if !test.MsgEqual(want, have) {
t.Fatalf("want: %v\nhave: %v", want, have)
t.Fatalf("response: want: %v\nhave: %v", want, have)
}
}

View File

@@ -7,11 +7,18 @@ import (
"github.com/coredns/coredns/middleware/dnstap/msg"
tap "github.com/dnstap/golang-dnstap"
"golang.org/x/net/context"
)
// Context is a message trap.
type Context struct {
context.Context
TrapTapper
}
// TestingData returns the Data matching coredns/test.ResponseWriter.
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"),
@@ -50,15 +57,24 @@ func toComp(m *tap.Message) comp {
}
}
// MsgEqual compares two dnstap messages ignoring timestamps.
func MsgEqual(a, b *tap.Message) bool {
return reflect.DeepEqual(toComp(a), toComp(b))
}
type TrapTaper struct {
// TrapTapper traps messages.
type TrapTapper struct {
Trap []*tap.Message
Full bool
}
func (t *TrapTaper) TapMessage(m *tap.Message) error {
// TapMessage adds the message to the trap.
func (t *TrapTapper) TapMessage(m *tap.Message) error {
t.Trap = append(t.Trap, m)
return nil
}
// TapBuilder returns a test msg.Builder.
func (t *TrapTapper) TapBuilder() msg.Builder {
return msg.Builder{Full: t.Full}
}