IP endpoint for dnstap (#1002)

* adds the option to log to a remote endpoint

* examples

* tests

* tcp:// or default to unix://

* cosmetic update

* bad naked returns
This commit is contained in:
varyoo
2017-09-01 14:07:21 +02:00
committed by Miek Gieben
parent c5efd45720
commit 345dee82ed
5 changed files with 200 additions and 20 deletions

View File

@@ -24,7 +24,13 @@ dnstap /tmp/dnstap.sock
Log information including the wire-format DNS message about client requests and responses to */tmp/dnstap.sock*.
~~~ txt
dnstap /tmp/dnstap.sock full
dnstap unix:///tmp/dnstap.sock full
~~~
Log to a remote endpoint.
~~~ txt
dnstap tcp://127.0.0.1:6000 full
~~~
## Dnstap command line tool
@@ -47,3 +53,9 @@ The following command listens on the given socket and saves message payloads to
~~~ 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
~~~

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

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

View File

@@ -2,11 +2,14 @@ package dnstap
import (
"fmt"
"io"
"log"
"strings"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/middleware"
"github.com/coredns/coredns/middleware/dnstap/out"
"github.com/coredns/coredns/middleware/pkg/dnsutil"
"github.com/mholt/caddy"
"github.com/mholt/caddy/caddyfile"
@@ -26,30 +29,55 @@ func wrapSetup(c *caddy.Controller) error {
return nil
}
func parseConfig(c *caddyfile.Dispenser) (path string, full bool, err error) {
c.Next() // directive name
type config struct {
target string
socket bool
full bool
}
if !c.Args(&path) {
err = c.ArgErr()
return
func parseConfig(d *caddyfile.Dispenser) (c config, err error) {
d.Next() // directive name
if !d.Args(&c.target) {
return c, d.ArgErr()
}
full = c.NextArg() && c.Val() == "full"
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 {
path, full, err := parseConfig(&c.Dispenser)
conf, err := parseConfig(&c.Dispenser)
if err != nil {
return err
}
dnstap := Dnstap{Pack: full}
dnstap := Dnstap{Pack: conf.full}
o, err := out.NewSocket(path)
if err != nil {
log.Printf("[WARN] Can't connect to %s at the moment", path)
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

View File

@@ -6,14 +6,29 @@ import (
)
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)
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},
}
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)
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)
}
}
}