mirror of
https://github.com/coredns/coredns.git
synced 2025-11-01 18:53:43 -04:00
Remove the word middleware (#1067)
* Rename middleware to plugin first pass; mostly used 'sed', few spots where I manually changed text. This still builds a coredns binary. * fmt error * Rename AddMiddleware to AddPlugin * Readd AddMiddleware to remain backwards compat
This commit is contained in:
61
plugin/dnstap/README.md
Normal file
61
plugin/dnstap/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# dnstap
|
||||
|
||||
*dnstap* enables logging to dnstap, a flexible, structured binary log format for DNS software: http://dnstap.info.
|
||||
|
||||
There is a buffer, expect at least 13 requests before the server sends its dnstap messages to the socket.
|
||||
|
||||
## Syntax
|
||||
|
||||
~~~ txt
|
||||
dnstap SOCKET [full]
|
||||
~~~
|
||||
|
||||
* **SOCKET** is the socket path supplied to the dnstap command line tool.
|
||||
* `full` to include the wire-format DNS message.
|
||||
|
||||
## Examples
|
||||
|
||||
Log information about client requests and responses to */tmp/dnstap.sock*.
|
||||
|
||||
~~~ txt
|
||||
dnstap /tmp/dnstap.sock
|
||||
~~~
|
||||
|
||||
Log information including the wire-format DNS message about client requests and responses to */tmp/dnstap.sock*.
|
||||
|
||||
~~~ txt
|
||||
dnstap unix:///tmp/dnstap.sock full
|
||||
~~~
|
||||
|
||||
Log to a remote endpoint.
|
||||
|
||||
~~~ txt
|
||||
dnstap tcp://127.0.0.1:6000 full
|
||||
~~~
|
||||
|
||||
## Dnstap command line tool
|
||||
|
||||
~~~ sh
|
||||
go get github.com/dnstap/golang-dnstap
|
||||
cd $GOPATH/src/github.com/dnstap/golang-dnstap/dnstap
|
||||
go build
|
||||
./dnstap
|
||||
~~~
|
||||
|
||||
The following command listens on the given socket and decodes messages to stdout.
|
||||
|
||||
~~~ sh
|
||||
dnstap -u /tmp/dnstap.sock
|
||||
~~~
|
||||
|
||||
The following command listens on the given socket and saves message payloads to a binary dnstap-format log file.
|
||||
|
||||
~~~ sh
|
||||
dnstap -u /tmp/dnstap.sock -w /tmp/test.dnstap
|
||||
~~~
|
||||
|
||||
Listen for dnstap messages on port 6000.
|
||||
|
||||
~~~ sh
|
||||
dnstap -l 127.0.0.1:6000
|
||||
~~~
|
||||
79
plugin/dnstap/handler.go
Normal file
79
plugin/dnstap/handler.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package dnstap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/dnstap/msg"
|
||||
"github.com/coredns/coredns/plugin/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 plugin.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 plugin 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 {
|
||||
return fmt.Errorf("marshal: %s", err)
|
||||
}
|
||||
_, err = w.Write(frame)
|
||||
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, Tapper: &h, Query: r}
|
||||
rw.QueryEpoch()
|
||||
|
||||
code, err := plugin.NextOrFailure(h.Name(), h.Next, tapContext{ctx, h}, rw, r)
|
||||
if err != nil {
|
||||
// ignore dnstap errors
|
||||
return code, err
|
||||
}
|
||||
|
||||
if err := rw.DnstapError(); err != nil {
|
||||
return code, plugin.Error("dnstap", err)
|
||||
}
|
||||
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// Name returns dnstap.
|
||||
func (h Dnstap) Name() string { return "dnstap" }
|
||||
65
plugin/dnstap/handler_test.go
Normal file
65
plugin/dnstap/handler_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package dnstap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/dnstap/test"
|
||||
mwtest "github.com/coredns/coredns/plugin/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)
|
||||
}
|
||||
168
plugin/dnstap/msg/msg.go
Normal file
168
plugin/dnstap/msg/msg.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Package msg helps to build a dnstap Message.
|
||||
package msg
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tap "github.com/dnstap/golang-dnstap"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Builder helps to build Data by being aware of the dnstap plugin 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 {
|
||||
Packed []byte
|
||||
SocketProto tap.SocketProtocol
|
||||
SocketFam tap.SocketFamily
|
||||
Address []byte
|
||||
Port uint32
|
||||
TimeSec uint64
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// Pack encodes the DNS message into Data.
|
||||
func (d *Data) Pack(m *dns.Msg) error {
|
||||
packed, err := m.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Packed = packed
|
||||
return nil
|
||||
}
|
||||
|
||||
// Epoch returns the epoch time in seconds.
|
||||
func Epoch() uint64 {
|
||||
return uint64(time.Now().Unix())
|
||||
}
|
||||
|
||||
// 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 {
|
||||
t := tap.Message_CLIENT_RESPONSE
|
||||
return &tap.Message{
|
||||
Type: &t,
|
||||
SocketFamily: &d.SocketFam,
|
||||
SocketProtocol: &d.SocketProto,
|
||||
ResponseTimeSec: &d.TimeSec,
|
||||
ResponseMessage: d.Packed,
|
||||
QueryAddress: d.Address,
|
||||
QueryPort: &d.Port,
|
||||
}
|
||||
}
|
||||
|
||||
// ToClientQuery transforms Data into a client query message.
|
||||
func (d *Data) ToClientQuery() *tap.Message {
|
||||
t := tap.Message_CLIENT_QUERY
|
||||
return &tap.Message{
|
||||
Type: &t,
|
||||
SocketFamily: &d.SocketFam,
|
||||
SocketProtocol: &d.SocketProto,
|
||||
QueryTimeSec: &d.TimeSec,
|
||||
QueryMessage: d.Packed,
|
||||
QueryAddress: d.Address,
|
||||
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,
|
||||
}
|
||||
}
|
||||
42
plugin/dnstap/msg/msg_test.go
Normal file
42
plugin/dnstap/msg/msg_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package msg
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/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.RemoteAddr(r.W.RemoteAddr()); 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}
|
||||
}
|
||||
26
plugin/dnstap/msg/wrapper.go
Normal file
26
plugin/dnstap/msg/wrapper.go
Normal file
@@ -0,0 +1,26 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
err = fmt.Errorf("proto: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
86
plugin/dnstap/out/socket.go
Normal file
86
plugin/dnstap/out/socket.go
Normal 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
|
||||
}
|
||||
94
plugin/dnstap/out/socket_test.go
Normal file
94
plugin/dnstap/out/socket_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
59
plugin/dnstap/out/tcp.go
Normal file
59
plugin/dnstap/out/tcp.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package out
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
fs "github.com/farsightsec/golang-framestream"
|
||||
)
|
||||
|
||||
// TCP is a Frame Streams encoder over TCP.
|
||||
type TCP struct {
|
||||
address string
|
||||
frames [][]byte
|
||||
}
|
||||
|
||||
// NewTCP returns a TCP writer.
|
||||
func NewTCP(address string) *TCP {
|
||||
s := &TCP{address: address}
|
||||
s.frames = make([][]byte, 0, 13) // 13 messages buffer
|
||||
return s
|
||||
}
|
||||
|
||||
// Write a single Frame Streams frame.
|
||||
func (s *TCP) Write(frame []byte) (n int, err error) {
|
||||
s.frames = append(s.frames, frame)
|
||||
if len(s.frames) == cap(s.frames) {
|
||||
return len(frame), s.Flush()
|
||||
}
|
||||
return len(frame), nil
|
||||
}
|
||||
|
||||
// Flush the remaining frames.
|
||||
func (s *TCP) Flush() error {
|
||||
defer func() {
|
||||
s.frames = s.frames[0:]
|
||||
}()
|
||||
c, err := net.DialTimeout("tcp", s.address, time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enc, err := fs.NewEncoder(c, &fs.EncoderOptions{
|
||||
ContentType: []byte("protobuf:dnstap.Dnstap"),
|
||||
Bidirectional: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, frame := range s.frames {
|
||||
if _, err = enc.Write(frame); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return enc.Flush()
|
||||
}
|
||||
|
||||
// Close is an alias to Flush to satisfy io.WriteCloser similarly to type Socket.
|
||||
func (s *TCP) Close() error {
|
||||
return s.Flush()
|
||||
}
|
||||
66
plugin/dnstap/out/tcp_test.go
Normal file
66
plugin/dnstap/out/tcp_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package out
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sendOneTCP(tcp *TCP) error {
|
||||
if _, err := tcp.Write([]byte("frame")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tcp.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func TestTCP(t *testing.T) {
|
||||
tcp := NewTCP("localhost:14000")
|
||||
|
||||
if err := sendOneTCP(tcp); err == nil {
|
||||
t.Fatal("Not listening but no error.")
|
||||
return
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", "localhost:14000")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
wait := make(chan bool)
|
||||
go func() {
|
||||
acceptOne(t, l)
|
||||
wait <- true
|
||||
}()
|
||||
|
||||
if err := sendOneTCP(tcp); err != nil {
|
||||
t.Fatalf("send one: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
<-wait
|
||||
|
||||
// TODO: When the server isn't responding according to the framestream protocol
|
||||
// the thread is blocked.
|
||||
/*
|
||||
if err := sendOneTCP(tcp); err == nil {
|
||||
panic("must fail")
|
||||
}
|
||||
*/
|
||||
|
||||
go func() {
|
||||
acceptOne(t, l)
|
||||
wait <- true
|
||||
}()
|
||||
|
||||
if err := sendOneTCP(tcp); err != nil {
|
||||
t.Fatalf("send one: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
<-wait
|
||||
if err := l.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
98
plugin/dnstap/setup.go
Normal file
98
plugin/dnstap/setup.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package dnstap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/dnstap/out"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
||||
|
||||
"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 plugin.Error("dnstap", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type config struct {
|
||||
target string
|
||||
socket bool
|
||||
full bool
|
||||
}
|
||||
|
||||
func parseConfig(d *caddyfile.Dispenser) (c config, err error) {
|
||||
d.Next() // directive name
|
||||
|
||||
if !d.Args(&c.target) {
|
||||
return c, d.ArgErr()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c.target, "tcp://") {
|
||||
// remote IP endpoint
|
||||
servers, err := dnsutil.ParseHostPortOrFile(c.target[6:])
|
||||
if err != nil {
|
||||
return c, d.ArgErr()
|
||||
}
|
||||
c.target = servers[0]
|
||||
} else {
|
||||
// default to UNIX socket
|
||||
if strings.HasPrefix(c.target, "unix://") {
|
||||
c.target = c.target[7:]
|
||||
}
|
||||
c.socket = true
|
||||
}
|
||||
|
||||
c.full = d.NextArg() && d.Val() == "full"
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
conf, err := parseConfig(&c.Dispenser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dnstap := Dnstap{Pack: conf.full}
|
||||
|
||||
var o io.WriteCloser
|
||||
if conf.socket {
|
||||
o, err = out.NewSocket(conf.target)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Can't connect to %s at the moment: %s", conf.target, err)
|
||||
}
|
||||
} else {
|
||||
o = out.NewTCP(conf.target)
|
||||
}
|
||||
dnstap.Out = o
|
||||
|
||||
c.OnShutdown(func() error {
|
||||
if err := o.Close(); err != nil {
|
||||
return fmt.Errorf("output: %s", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(
|
||||
func(next plugin.Handler) plugin.Handler {
|
||||
dnstap.Next = next
|
||||
return dnstap
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
34
plugin/dnstap/setup_test.go
Normal file
34
plugin/dnstap/setup_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package dnstap
|
||||
|
||||
import (
|
||||
"github.com/mholt/caddy"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
file string
|
||||
path string
|
||||
full bool
|
||||
socket bool
|
||||
fail bool
|
||||
}{
|
||||
{"dnstap dnstap.sock full", "dnstap.sock", true, true, false},
|
||||
{"dnstap unix://dnstap.sock", "dnstap.sock", false, true, false},
|
||||
{"dnstap tcp://127.0.0.1:6000", "127.0.0.1:6000", false, false, false},
|
||||
{"dnstap", "fail", false, true, true},
|
||||
}
|
||||
for _, c := range tests {
|
||||
cad := caddy.NewTestController("dns", c.file)
|
||||
conf, err := parseConfig(&cad.Dispenser)
|
||||
if c.fail {
|
||||
if err == nil {
|
||||
t.Errorf("%s: %s", c.file, err)
|
||||
}
|
||||
} else if err != nil || conf.target != c.path ||
|
||||
conf.full != c.full || conf.socket != c.socket {
|
||||
|
||||
t.Errorf("expected: %+v\nhave: %+v\nerror: %s\n", c, conf, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
73
plugin/dnstap/taprw/writer.go
Normal file
73
plugin/dnstap/taprw/writer.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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/plugin/dnstap/msg"
|
||||
|
||||
tap "github.com/dnstap/golang-dnstap"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
queryEpoch uint64
|
||||
Query *dns.Msg
|
||||
dns.ResponseWriter
|
||||
Tapper
|
||||
err error
|
||||
}
|
||||
|
||||
// DnstapError check if a dnstap error occurred during Write and returns it.
|
||||
func (w ResponseWriter) DnstapError() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
// QueryEpoch sets the query epoch as reported by dnstap.
|
||||
func (w *ResponseWriter) QueryEpoch() {
|
||||
w.queryEpoch = msg.Epoch()
|
||||
}
|
||||
|
||||
// WriteMsg writes back the response to the client and THEN works on logging the request
|
||||
// and response to dnstap.
|
||||
// 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()
|
||||
|
||||
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 := 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
|
||||
}
|
||||
82
plugin/dnstap/taprw/writer_test.go
Normal file
82
plugin/dnstap/taprw/writer_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package taprw
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin/dnstap/msg"
|
||||
"github.com/coredns/coredns/plugin/dnstap/test"
|
||||
mwtest "github.com/coredns/coredns/plugin/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 (TapFailer) TapBuilder() msg.Builder {
|
||||
return msg.Builder{Full: true}
|
||||
}
|
||||
|
||||
func TestDnstapError(t *testing.T) {
|
||||
rw := ResponseWriter{
|
||||
Query: new(dns.Msg),
|
||||
ResponseWriter: &mwtest.ResponseWriter{},
|
||||
Tapper: 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 TestClientQueryResponse(t *testing.T) {
|
||||
trapper := test.TrapTapper{Full: true}
|
||||
m := testingMsg()
|
||||
rw := ResponseWriter{
|
||||
Query: m,
|
||||
Tapper: &trapper,
|
||||
ResponseWriter: &mwtest.ResponseWriter{},
|
||||
}
|
||||
d := test.TestingData()
|
||||
|
||||
// will the wire-format msg be reported?
|
||||
bin, err := m.Pack()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
d.Packed = bin
|
||||
|
||||
if err := rw.WriteMsg(m); err != nil {
|
||||
t.Fatal(err)
|
||||
return
|
||||
}
|
||||
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("query: want: %v\nhave: %v", want, have)
|
||||
}
|
||||
want = d.ToClientResponse()
|
||||
have = trapper.Trap[1]
|
||||
if !test.MsgEqual(want, have) {
|
||||
t.Fatalf("response: want: %v\nhave: %v", want, have)
|
||||
}
|
||||
}
|
||||
80
plugin/dnstap/test/helpers.go
Normal file
80
plugin/dnstap/test/helpers.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"net"
|
||||
"reflect"
|
||||
|
||||
"github.com/coredns/coredns/plugin/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{
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// MsgEqual compares two dnstap messages ignoring timestamps.
|
||||
func MsgEqual(a, b *tap.Message) bool {
|
||||
return reflect.DeepEqual(toComp(a), toComp(b))
|
||||
}
|
||||
|
||||
// TrapTapper traps messages.
|
||||
type TrapTapper struct {
|
||||
Trap []*tap.Message
|
||||
Full bool
|
||||
}
|
||||
|
||||
// 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}
|
||||
}
|
||||
Reference in New Issue
Block a user