Browse Source

Fix dnsflagday issues

Also:
- NSID support
- Support for DNS Cookies

(closes #115)
Ask Bjørn Hansen 5 years ago
parent
commit
b3ca7047d2
8 changed files with 213 additions and 46 deletions
  1. 6 0
      CHANGES.md
  2. 2 1
      build
  3. 3 0
      edns/README.md
  4. 137 0
      edns/edns.go
  5. 4 4
      geodns-logs/process-stats.go
  6. 2 2
      go.sum
  7. 49 37
      server/serve.go
  8. 10 2
      server/server.go

+ 6 - 0
CHANGES.md

@@ -1,5 +1,11 @@
 # GeoDNS Changelog
 
+## 3.1.0 - in development
+
+* NSID support
+* Support for DNS Cookies
+* dnsflagday cleanups
+
 ## 3.0.2 December 2019
 
 * Better test errors when geoip2 files aren't found

+ 2 - 1
build

@@ -14,10 +14,11 @@ ARCH=${GOARCH:-`go env GOARCH`}
 set -ex
 
 go build -o dist/geodns-$OS-$ARCH \
+  -mod=vendor \
   -trimpath \
   -ldflags "-X main.gitVersion=$REVISION -X main.buildTime=$BUILDTIME" \
   -v && \
-  (cd geodns-logs && go build -v -o ../dist/geodns-logs-$OS-$ARCH && cd ..) && \
+  (cd geodns-logs && go build -trimpath -mod=vendor -v -o ../dist/geodns-logs-$OS-$ARCH && cd ..) && \
   cd dist && \
   rm -f service service-logs && \
   ln -s ../service . && \

+ 3 - 0
edns/README.md

@@ -0,0 +1,3 @@
+
+
+This is from github.com/coredns/coredns/plugin/pkg/edns/

+ 137 - 0
edns/edns.go

@@ -0,0 +1,137 @@
+// Package edns provides function useful for adding/inspecting OPT records to/in messages.
+package edns
+
+import (
+	"errors"
+	"sync"
+
+	"github.com/miekg/dns"
+)
+
+var sup = &supported{m: make(map[uint16]struct{})}
+
+type supported struct {
+	m map[uint16]struct{}
+	sync.RWMutex
+}
+
+// SetSupportedOption adds a new supported option the set of EDNS0 options that we support. Plugins typically call
+// this in their setup code to signal support for a new option.
+// By default we support:
+// dns.EDNS0NSID, dns.EDNS0EXPIRE, dns.EDNS0COOKIE, dns.EDNS0TCPKEEPALIVE, dns.EDNS0PADDING. These
+// values are not in this map and checked directly in the server.
+func SetSupportedOption(option uint16) {
+	sup.Lock()
+	sup.m[option] = struct{}{}
+	sup.Unlock()
+}
+
+// SupportedOption returns true if the option code is supported as an extra EDNS0 option.
+func SupportedOption(option uint16) bool {
+	sup.RLock()
+	_, ok := sup.m[option]
+	sup.RUnlock()
+	return ok
+}
+
+// Version checks the EDNS version in the request. If error
+// is nil everything is OK and we can invoke the plugin. If non-nil, the
+// returned Msg is valid to be returned to the client (and should). For some
+// reason this response should not contain a question RR in the question section.
+func Version(req *dns.Msg) (*dns.Msg, error) {
+	opt := req.IsEdns0()
+	if opt == nil {
+		return nil, nil
+	}
+	if opt.Version() == 0 {
+		return nil, nil
+	}
+	m := new(dns.Msg)
+	m.SetReply(req)
+	// zero out question section, wtf.
+	m.Question = nil
+
+	o := new(dns.OPT)
+	o.Hdr.Name = "."
+	o.Hdr.Rrtype = dns.TypeOPT
+	o.SetVersion(0)
+	m.Rcode = dns.RcodeBadVers
+	o.SetExtendedRcode(dns.RcodeBadVers)
+	m.Extra = []dns.RR{o}
+
+	return m, errors.New("EDNS0 BADVERS")
+}
+
+// Size returns a normalized size based on proto.
+func Size(proto string, size uint16) uint16 {
+	if proto == "tcp" {
+		return dns.MaxMsgSize
+	}
+	if size < dns.MinMsgSize {
+		return dns.MinMsgSize
+	}
+	return size
+}
+
+/*
+
+The below wasn't from the edns package
+
+*/
+
+// SetSizeAndDo adds an OPT record that the reflects the intent from request.
+func SetSizeAndDo(req, m *dns.Msg) *dns.OPT {
+	o := req.IsEdns0()
+	if o == nil {
+		return nil
+	}
+
+	if mo := m.IsEdns0(); mo != nil {
+		mo.Hdr.Name = "."
+		mo.Hdr.Rrtype = dns.TypeOPT
+		mo.SetVersion(0)
+		mo.SetUDPSize(o.UDPSize())
+		mo.Hdr.Ttl &= 0xff00 // clear flags
+
+		// Assume if the message m has options set, they are OK and represent what an upstream can do.
+
+		if o.Do() {
+			mo.SetDo()
+		}
+		return mo
+	}
+
+	// Reuse the request's OPT record and tack it to m.
+	o.Hdr.Name = "."
+	o.Hdr.Rrtype = dns.TypeOPT
+	o.SetVersion(0)
+	o.Hdr.Ttl &= 0xff00 // clear flags
+
+	if len(o.Option) > 0 {
+		o.Option = SupportedOptions(o.Option)
+	}
+
+	m.Extra = append(m.Extra, o)
+	return o
+}
+
+func SupportedOptions(o []dns.EDNS0) []dns.EDNS0 {
+	var supported = make([]dns.EDNS0, 0, 3)
+	// For as long as possible try avoid looking up in the map, because that need an Rlock.
+	for _, opt := range o {
+		switch code := opt.Option(); code {
+		case dns.EDNS0NSID:
+			fallthrough
+		case dns.EDNS0COOKIE:
+			fallthrough
+		case dns.EDNS0SUBNET:
+			supported = append(supported, opt)
+		default:
+			if SupportedOption(code) {
+				supported = append(supported, opt)
+
+			}
+		}
+	}
+	return supported
+}

+ 4 - 4
geodns-logs/process-stats.go

@@ -10,8 +10,8 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/nxadm/tail"
 	"github.com/miekg/dns"
+	"github.com/nxadm/tail"
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 
@@ -23,11 +23,11 @@ import (
 // Add vendor yes/no
 // add server region tag (identifier)?
 
-const UserAgent = "geodns-logs/2.0"
+const userAgent = "geodns-logs/2.0"
 
 func main() {
 
-	log.Printf("Starting %q", UserAgent)
+	log.Printf("Starting %q", userAgent)
 
 	identifierFlag := flag.String("identifier", "", "identifier (hostname, pop name or similar)")
 	// verboseFlag := flag.Bool("verbose", false, "verbose output")
@@ -70,7 +70,7 @@ func main() {
 		[]string{"Version"},
 	)
 	prometheus.MustRegister(buildInfo)
-	buildInfo.WithLabelValues(UserAgent).Set(1)
+	buildInfo.WithLabelValues(userAgent).Set(1)
 
 	http.Handle("/metrics", promhttp.Handler())
 	go func() {

+ 2 - 2
go.sum

@@ -1,5 +1,7 @@
 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/abh/dns v1.1.26-1 h1:Q5xZ912xwwCbcqHVwEM4ZDyukU86CsPl0Mf9g2X79vs=
+github.com/abh/dns v1.1.26-1/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/abh/errorutil v0.0.0-20130729183701-f9bd360d00b9 h1:xwBQqR2Uq0q7FDzd1z9lr/sB6NrvMBvOBW5xzDn+DcM=
 github.com/abh/errorutil v0.0.0-20130729183701-f9bd360d00b9/go.mod h1:L6nuAnQTJ2ZqobKWmzIcnTvpoOSWV8LulEOCk2rdhmM=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -47,8 +49,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU=
-github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=

+ 49 - 37
server/serve.go

@@ -1,6 +1,7 @@
 package server
 
 import (
+	"encoding/hex"
 	"encoding/json"
 	"fmt"
 	"net"
@@ -10,6 +11,7 @@ import (
 	"time"
 
 	"github.com/abh/geodns/applog"
+	"github.com/abh/geodns/edns"
 	"github.com/abh/geodns/querylog"
 	"github.com/abh/geodns/zones"
 
@@ -66,36 +68,28 @@ func (srv *Server) serve(w dns.ResponseWriter, req *dns.Msg, z *zones.Zone) {
 
 	z.Metrics.ClientStats.Add(realIP.String())
 
-	var ip net.IP // EDNS or real IP
-	var edns *dns.EDNS0_SUBNET
-	var opt_rr *dns.OPT
-
-	for _, extra := range req.Extra {
-
-		switch extra.(type) {
-		case *dns.OPT:
-			for _, o := range extra.(*dns.OPT).Option {
-				opt_rr = extra.(*dns.OPT)
-				switch e := o.(type) {
-				case *dns.EDNS0_NSID:
-					// do stuff with e.Nsid
-				case *dns.EDNS0_SUBNET:
-					applog.Println("Got edns", e.Address, e.Family, e.SourceNetmask, e.SourceScope)
-					if e.Address != nil {
-						edns = e
-						ip = e.Address
-
-						if qle != nil {
-							qle.HasECS = true
-							qle.ClientAddr = fmt.Sprintf("%s/%d", ip, e.SourceNetmask)
-						}
+	var ip net.IP // EDNS CLIENT SUBNET or real IP
+	var ecs *dns.EDNS0_SUBNET
+
+	if option := req.IsEdns0(); option != nil {
+		for _, s := range option.Option {
+			switch e := s.(type) {
+			case *dns.EDNS0_SUBNET:
+				applog.Println("Got edns-client-subnet", e.Address, e.Family, e.SourceNetmask, e.SourceScope)
+				if e.Address != nil {
+					ecs = e
+					ip = e.Address
+
+					if qle != nil {
+						qle.HasECS = true
+						qle.ClientAddr = fmt.Sprintf("%s/%d", ip, e.SourceNetmask)
 					}
 				}
 			}
 		}
 	}
 
-	if len(ip) == 0 { // no edns subnet
+	if len(ip) == 0 { // no edns client subnet
 		ip = realIP
 		if qle != nil {
 			qle.ClientAddr = fmt.Sprintf("%s/%d", ip, len(ip)*8)
@@ -104,8 +98,9 @@ func (srv *Server) serve(w dns.ResponseWriter, req *dns.Msg, z *zones.Zone) {
 
 	targets, netmask, location := z.Options.Targeting.GetTargets(ip, z.HasClosest)
 
-	m := new(dns.Msg)
+	m := &dns.Msg{}
 
+	// setup logging of answers and rcode
 	if qle != nil {
 		qle.Targets = targets
 		defer func() {
@@ -114,23 +109,40 @@ func (srv *Server) serve(w dns.ResponseWriter, req *dns.Msg, z *zones.Zone) {
 		}()
 	}
 
-	m.SetReply(req)
-	if e := m.IsEdns0(); e != nil {
-		m.SetEdns0(4096, e.Do())
+	mv, err := edns.Version(req)
+	if err != nil {
+		m = mv
+		err := w.WriteMsg(m)
+		if err != nil {
+			applog.Printf("could not write response: %s", err)
+		}
+		return
 	}
-	m.Authoritative = true
 
-	// TODO: set scope to 0 if there are no alternate responses
-	if edns != nil {
-		if edns.Family != 0 {
-			if netmask < 16 {
-				netmask = 16
+	m.SetReply(req)
+
+	if option := edns.SetSizeAndDo(req, m); option != nil {
+
+		for _, s := range option.Option {
+			switch e := s.(type) {
+			case *dns.EDNS0_NSID:
+				e.Code = dns.EDNS0NSID
+				e.Nsid = hex.EncodeToString([]byte(srv.info.ID))
+			case *dns.EDNS0_SUBNET:
+				// access e.Family, e.Address, etc.
+				// TODO: set scope to 0 if there are no alternate responses
+				if ecs.Family != 0 {
+					if netmask < 16 {
+						netmask = 16
+					}
+					e.SourceScope = uint8(netmask)
+				}
 			}
-			edns.SourceScope = uint8(netmask)
-			m.Extra = append(m.Extra, opt_rr)
 		}
 	}
 
+	m.Authoritative = true
+
 	labelMatches := z.FindLabels(qlabel, targets, []uint16{dns.TypeMF, dns.TypeCNAME, qtype})
 
 	if len(labelMatches) == 0 {
@@ -267,7 +279,7 @@ func (srv *Server) serve(w dns.ResponseWriter, req *dns.Msg, z *zones.Zone) {
 		// should this be in the match loop above?
 		qle.Rcode = m.Rcode
 	}
-	err := w.WriteMsg(m)
+	err = w.WriteMsg(m)
 	if err != nil {
 		// if Pack'ing fails the Write fails. Return SERVFAIL.
 		applog.Printf("Error writing packet: %q, %s", err, m)

+ 10 - 2
server/server.go

@@ -15,6 +15,7 @@ type serverMetrics struct {
 	Queries *prometheus.CounterVec
 }
 
+// Server ...
 type Server struct {
 	queryLogger        querylog.QueryLogger
 	mux                *dns.ServeMux
@@ -23,6 +24,7 @@ type Server struct {
 	metrics            *serverMetrics
 }
 
+// NewServer ...
 func NewServer(si *monitor.ServerInfo) *Server {
 	mux := dns.NewServeMux()
 
@@ -68,16 +70,18 @@ func NewServer(si *monitor.ServerInfo) *Server {
 	return &Server{mux: mux, info: si, metrics: metrics}
 }
 
-// Setup the QueryLogger. For now it only supports writing to a file (and all
-// zones get logged to the same file).
+// SetQueryLogger configures the query logger. For now it only supports writing to
+// a file (and all zones get logged to the same file).
 func (srv *Server) SetQueryLogger(logger querylog.QueryLogger) {
 	srv.queryLogger = logger
 }
 
+// Add adds the Zone to be handled under the specified name
 func (srv *Server) Add(name string, zone *zones.Zone) {
 	srv.mux.HandleFunc(name, srv.setupServerFunc(zone))
 }
 
+// Remove removes the zone name from being handled by the server
 func (srv *Server) Remove(name string) {
 	srv.mux.HandleRemove(name)
 }
@@ -88,10 +92,14 @@ func (srv *Server) setupServerFunc(zone *zones.Zone) func(dns.ResponseWriter, *d
 	}
 }
 
+// ServeDNS calls ServeDNS in the dns package
 func (srv *Server) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 	srv.mux.ServeDNS(w, r)
 }
 
+// ListenAndServe starts the DNS server on the specified IP
+// (both tcp and udp) and returns. If something goes wrong
+// it will crash the process with an error message.
 func (srv *Server) ListenAndServe(ip string) {
 
 	prots := []string{"udp", "tcp"}