From c5efd45720a4e91569ac70f0f8b8aeaa4576e508 Mon Sep 17 00:00:00 2001 From: varyoo Date: Fri, 1 Sep 2017 12:41:41 +0200 Subject: [PATCH] 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 --- middleware/dnstap/handler.go | 35 +++++++- middleware/dnstap/msg/msg.go | 106 ++++++++++++++++++++++--- middleware/dnstap/msg/msg_test.go | 2 +- middleware/dnstap/msg/wrapper.go | 1 + middleware/dnstap/taprw/writer.go | 77 ++++++++---------- middleware/dnstap/taprw/writer_test.go | 48 ++++------- middleware/dnstap/test/helpers.go | 22 ++++- middleware/proxy/dns.go | 8 ++ middleware/proxy/dnstap_test.go | 57 +++++++++++++ middleware/proxy/exchanger.go | 4 + middleware/proxy/google.go | 4 + middleware/proxy/grpc.go | 2 + middleware/proxy/proxy.go | 48 ++++++++++- 13 files changed, 317 insertions(+), 97 deletions(-) create mode 100644 middleware/proxy/dnstap_test.go diff --git a/middleware/dnstap/handler.go b/middleware/dnstap/handler.go index 0cf281e69..eb2924be5 100644 --- a/middleware/dnstap/handler.go +++ b/middleware/dnstap/handler.go @@ -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" } diff --git a/middleware/dnstap/msg/msg.go b/middleware/dnstap/msg/msg.go index 97c7ec7cc..1d42ea3ba 100644 --- a/middleware/dnstap/msg/msg.go +++ b/middleware/dnstap/msg/msg.go @@ -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, + } +} diff --git a/middleware/dnstap/msg/msg_test.go b/middleware/dnstap/msg/msg_test.go index 6a54a9e8c..2f80a90cd 100644 --- a/middleware/dnstap/msg/msg_test.go +++ b/middleware/dnstap/msg/msg_test.go @@ -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 } diff --git a/middleware/dnstap/msg/wrapper.go b/middleware/dnstap/msg/wrapper.go index 0cb6a76c0..a74c604d8 100644 --- a/middleware/dnstap/msg/wrapper.go +++ b/middleware/dnstap/msg/wrapper.go @@ -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 { diff --git a/middleware/dnstap/taprw/writer.go b/middleware/dnstap/taprw/writer.go index 6f4af330f..99572afd9 100644 --- a/middleware/dnstap/taprw/writer.go +++ b/middleware/dnstap/taprw/writer.go @@ -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 } diff --git a/middleware/dnstap/taprw/writer_test.go b/middleware/dnstap/taprw/writer_test.go index a19271e7c..426f1f580 100644 --- a/middleware/dnstap/taprw/writer_test.go +++ b/middleware/dnstap/taprw/writer_test.go @@ -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) } } diff --git a/middleware/dnstap/test/helpers.go b/middleware/dnstap/test/helpers.go index fba291dfe..46ba327ab 100644 --- a/middleware/dnstap/test/helpers.go +++ b/middleware/dnstap/test/helpers.go @@ -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} +} diff --git a/middleware/proxy/dns.go b/middleware/proxy/dns.go index c1c0ad078..4d8038422 100644 --- a/middleware/proxy/dns.go +++ b/middleware/proxy/dns.go @@ -28,6 +28,14 @@ func newDNSExWithOption(opt Options) *dnsEx { return &dnsEx{Timeout: defaultTimeout * time.Second, Options: opt} } +func (d *dnsEx) Transport() string { + if d.Options.ForceTCP { + return "tcp" + } + + // The protocol will be determined by `state.Proto()` during Exchange. + return "" +} func (d *dnsEx) Protocol() string { return "dns" } func (d *dnsEx) OnShutdown(p *Proxy) error { return nil } func (d *dnsEx) OnStartup(p *Proxy) error { return nil } diff --git a/middleware/proxy/dnstap_test.go b/middleware/proxy/dnstap_test.go new file mode 100644 index 000000000..b3c31c207 --- /dev/null +++ b/middleware/proxy/dnstap_test.go @@ -0,0 +1,57 @@ +package proxy + +import ( + "testing" + + "github.com/coredns/coredns/middleware/dnstap/msg" + "github.com/coredns/coredns/middleware/dnstap/test" + mwtest "github.com/coredns/coredns/middleware/test" + "github.com/coredns/coredns/request" + + tap "github.com/dnstap/golang-dnstap" + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +func testCase(t *testing.T, ex Exchanger, q, r *dns.Msg, datq, datr *msg.Data) { + tapq := datq.ToOutsideQuery(tap.Message_FORWARDER_QUERY) + tapr := datr.ToOutsideResponse(tap.Message_FORWARDER_RESPONSE) + ctx := test.Context{} + err := toDnstap(&ctx, "10.240.0.1:40212", ex, + request.Request{W: &mwtest.ResponseWriter{}, Req: q}, r, 0, 0) + if err != nil { + t.Fatal(err) + } + if len(ctx.Trap) != 2 { + t.Fatalf("messages: %d", len(ctx.Trap)) + } + if !test.MsgEqual(ctx.Trap[0], tapq) { + t.Errorf("want: %v\nhave: %v", tapq, ctx.Trap[0]) + } + if !test.MsgEqual(ctx.Trap[1], tapr) { + t.Errorf("want: %v\nhave: %v", tapr, ctx.Trap[1]) + } +} + +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, tapr := test.TestingData(), test.TestingData() + testCase(t, newDNSEx(), q, r, tapq, tapr) + tapq.SocketProto = tap.SocketProtocol_TCP + tapr.SocketProto = tap.SocketProtocol_TCP + testCase(t, newDNSExWithOption(Options{ForceTCP: true}), q, r, tapq, tapr) + testCase(t, newGoogle("", []string{"8.8.8.8:53", "8.8.4.4:53"}), q, r, tapq, tapr) +} + +func TestNoDnstap(t *testing.T) { + err := toDnstap(context.TODO(), "", nil, request.Request{}, nil, 0, 0) + if err != nil { + t.Fatal(err) + } +} diff --git a/middleware/proxy/exchanger.go b/middleware/proxy/exchanger.go index 28e1b1b11..b98a687e7 100644 --- a/middleware/proxy/exchanger.go +++ b/middleware/proxy/exchanger.go @@ -13,6 +13,10 @@ type Exchanger interface { Exchange(ctx context.Context, addr string, state request.Request) (*dns.Msg, error) Protocol() string + // Transport returns the only transport protocol used by this Exchanger or "". + // If the return value is "", Exchange must use `state.Proto()`. + Transport() string + OnStartup(*Proxy) error OnShutdown(*Proxy) error } diff --git a/middleware/proxy/google.go b/middleware/proxy/google.go index b71d0fb1b..7b215f517 100644 --- a/middleware/proxy/google.go +++ b/middleware/proxy/google.go @@ -112,6 +112,10 @@ func (g *google) exchangeJSON(addr, json string) ([]byte, error) { return buf, nil } +func (g *google) Transport() string { + return "tcp" +} + func (g *google) Protocol() string { return "https_google" } func (g *google) OnShutdown(p *Proxy) error { diff --git a/middleware/proxy/grpc.go b/middleware/proxy/grpc.go index 031869c60..8aabf0eb0 100644 --- a/middleware/proxy/grpc.go +++ b/middleware/proxy/grpc.go @@ -54,6 +54,8 @@ func (g *grpcClient) Exchange(ctx context.Context, addr string, state request.Re return d, nil } +func (g *grpcClient) Transport() string { return "tcp" } + func (g *grpcClient) Protocol() string { return "grpc" } func (g *grpcClient) OnShutdown(p *Proxy) error { diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go index 7e662c42e..cef58f658 100644 --- a/middleware/proxy/proxy.go +++ b/middleware/proxy/proxy.go @@ -7,9 +7,12 @@ import ( "time" "github.com/coredns/coredns/middleware" + "github.com/coredns/coredns/middleware/dnstap" + "github.com/coredns/coredns/middleware/dnstap/msg" "github.com/coredns/coredns/middleware/pkg/healthcheck" "github.com/coredns/coredns/request" + tap "github.com/dnstap/golang-dnstap" "github.com/miekg/dns" ot "github.com/opentracing/opentracing-go" "golang.org/x/net/context" @@ -85,22 +88,28 @@ func (p Proxy) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( } atomic.AddInt64(&host.Conns, 1) + queryEpoch := msg.Epoch() reply, backendErr := upstream.Exchanger().Exchange(ctx, host.Name, state) + respEpoch := msg.Epoch() atomic.AddInt64(&host.Conns, -1) if child != nil { child.Finish() } + taperr := toDnstap(ctx, host.Name, upstream.Exchanger(), state, reply, + queryEpoch, respEpoch) + if backendErr == nil { w.WriteMsg(reply) RequestDuration.WithLabelValues(state.Proto(), upstream.Exchanger().Protocol(), upstream.From()).Observe(float64(time.Since(start) / time.Millisecond)) - return 0, nil + return 0, taperr } + timeout := host.FailTimeout if timeout == 0 { timeout = 10 * time.Second @@ -145,3 +154,40 @@ func (p Proxy) Name() string { return "proxy" } // defaultTimeout is the default networking timeout for DNS requests. const defaultTimeout = 5 * time.Second + +func toDnstap(ctx context.Context, host string, ex Exchanger, state request.Request, reply *dns.Msg, queryEpoch, respEpoch uint64) (err error) { + if tapper := dnstap.TapperFromContext(ctx); tapper != nil { + // Query + b := tapper.TapBuilder() + b.TimeSec = queryEpoch + if err = b.HostPort(host); err != nil { + return + } + t := ex.Transport() + if t == "" { + t = state.Proto() + } + if t == "tcp" { + b.SocketProto = tap.SocketProtocol_TCP + } else { + b.SocketProto = tap.SocketProtocol_UDP + } + if err = b.Msg(state.Req); err != nil { + return + } + err = tapper.TapMessage(b.ToOutsideQuery(tap.Message_FORWARDER_QUERY)) + if err != nil { + return + } + + // Response + if reply != nil { + b.TimeSec = respEpoch + if err = b.Msg(reply); err != nil { + return + } + err = tapper.TapMessage(b.ToOutsideResponse(tap.Message_FORWARDER_RESPONSE)) + } + } + return +}