dnsrouter

package module
v0.0.0-...-64bb39f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2018 License: MIT Imports: 12 Imported by: 0

README

Build Status codecov Go Report Card GoDoc

DnsRouter

DnsRouter is a lightweight high performance DNS request router with chaining middlewares.

Highly inspired by julienschmidt's HttpRouter and miekg's CoreDNS, actually, this router is developed upon HttpRouter's extremely fast radix tree, and passed all test cases of file plugin from CoreDNS.

In contrast to CoreDNS which is a complete DNS server, DnsRouter is targeting a library of a tree of stub name servers, all other resolving functions like filling out ANSWER, AUTHORITY, ADDITIONAL sections are designed as middlewares, which makes name server efficient and flexible.

Features

Named parameters in routing patterns: Directly inheriting parameterized patterns from HttpRouter, includes both :param and *catchAll, anyone who confuses wildcard DNS records or used to conventional HTTP mux patterns would feel easy to use it.

Anonymous parameters in routing patterns: Against named parameters in routing patterns, an anonymous asterisk in the beginning of domain patterns, e.g. *., is interpreted in DNS wildcard semantics, as RFC 4592, which makes DnsRouter compatible with traditional DNS wildcard matching rules.

Multi-Zone in one tree: A router instance could safely contain multiple zones simultaneously, the underlying radix tree promises the best performance if you have lots of records with lots of domains, or zones.

Nearly Zero Garbage: As HttpRouter, the tree related processes generate zero bytes of garbage, the actual up to 4 more heap allocations that are made, is from zone slice (1 alloc), domain name reversing (2 allocs), and a returning of an interface (1 alloc).

Out-of-box stub name server: The builtin middlewares are organized into two schemes, DefaultScheme and SimpleScheme. Use these schemes make DnsRouter working as an out-of-box stub name server, i.e. looking up name records, following CNAME redirection, expanding wildcards, supplying DNSSEC RRset and recovering panic, etc. What the different of DefaultScheme and SimpleScheme is that the later doesn't filling out AUTHORITY and ADDITIONAL sections.

Chaining middlewares: DnsRoute scales well by chaining middlewares, enjoyably choose what you need from builtin middlewares or implement your owns to extend stub name server, e.g. a recursive resolver or cache.

Fast: Benchmarks show DnsRouter is 2x to 4x faster than file plugin of CoreDNS.

Dependencies

Golang 1.9.x and miekg's awesome DNS library.

Install

Using the default go tool commands:

go get -v github.com/vegertar/dnsrouter

Usage

Let's start with a trivial example:

package main

import (
	"context"

	"github.com/miekg/dns"
	"github.com/vegertar/dnsrouter"
)

func main() {
	router := dnsrouter.New()
	router.HandleFunc("local A", func(w dnsrouter.ResponseWriter, req *dnsrouter.Request) {
		lo, err := dns.NewRR("local A 127.0.0.1")
		if err != nil {
			panic(err)
		}

		result := w.Msg()
		result.Answer = append(result.Answer, lo)
	})

	err := dns.ListenAndServe(":10053", "udp", dnsrouter.Classic(context.Background(), router))
	if err != nil {
		panic(err)
	}
}

Tests with dig command and omits unnecessary output, the sample print is shown below.

$ dig @127.0.0.1 -p 10053 local

;; ANSWER SECTION:
local.                  3600    IN      A       127.0.0.1

Then adds a SRV record.

	srv := new(dns.SRV)
	srv.Hdr.Name = "_dns._udp."
	srv.Hdr.Rrtype = dns.TypeSRV
	srv.Hdr.Class = dns.ClassINET
	srv.Port = 10053
	srv.Target = "local."

	router.HandleFunc("local. SRV", func(w dnsrouter.ResponseWriter, req *dnsrouter.Request) {
		result := w.Msg()
		result.Answer = append(result.Answer, srv)
	})

dig with SRV could display the service, with additional A record that we set before as well.

$ dig @127.0.0.1 -p 10053 local SRV

;; ANSWER SECTION:
_dns._udp.              0       IN      SRV     0 0 10053 local.

;; ADDITIONAL SECTION:
local.                  3600    IN      A       127.0.0.1

Alternatively, you could try dig with ANY type.

$ dig @127.0.0.1 -p 10053 local ANY

;; ANSWER SECTION:
local.                  3600    IN      A       127.0.0.1
_dns._udp.              0       IN      SRV     0 0 10053 local.

Wants to known what happens when raises an exception? Adds some lines like below.

	router.HandleFunc("local. SRV", func(w dnsrouter.ResponseWriter, req *dnsrouter.Request) {
		panic("oops: an exception")
	})

dig with SRV option again.

$ dig @127.0.0.1 -p 10053 local. SRV

;; ANSWER SECTION:
_dns._udp.              0       IN      SRV     0 0 10053 local.

;; ADDITIONAL SECTION:
local.                  0       IN      TXT     "panic" "oops: an exception" "main.main.func3:35"

This time the ADDITIONAL section contains a TXT record instead, which describes errors in shortly, includes a flag literal string "panic", an error message, and the trace information.

All above records are writing out by builtin middlewares, but there is no logging middleware to log every incoming DNS queries, let's implement a simple logger in here.

func LoggerHandler(h dnsrouter.Handler) dnsrouter.Handler {
	return dnsrouter.HandlerFunc(func(w dnsrouter.ResponseWriter, req *dnsrouter.Request) {
		since := time.Now()
		q := req.Question[0]

		defer func() {
			log.Printf(`"%s %s %s" %s %v`,
				dns.TypeToString[q.Qtype],
				dns.ClassToString[q.Qclass],
				q.Name,
				dns.RcodeToString[w.Msg().Rcode],
				time.Since(since).String())
		}()

		h.ServeDNS(w, req)
	})
}

Then insert the LoggerHandler into chains.

	router.Middleware = append(router.Middleware, LoggerHandler)
	router.Middleware = append(router.Middleware, dnsrouter.DefaultScheme...)

Running and testing again.

$ go run a.go
2018/02/05 15:57:01 "SRV IN local." SERVFAIL 46.248µs
2018/02/05 15:57:07 "A IN local." NOERROR 139.3µs
2018/02/05 15:57:10 "ANY IN local." SERVFAIL 204.684µs
2018/02/05 15:58:41 "A IN hello." REFUSED 34.333µs
Named parameters & Catch-All parameters

These features are derived from HttpRouter, the only difference is that DnsRouter uses dot ('.') as the label separator, and matches from right to left.

Pattern: :user.example.org.

joe.example.org                 match, captures "joe"
lily.example.org                match, captures "lily"
zuck.mark.example.org           no match
example.org.                    no match
Pattern: *user.example.org.

joe.example.org                 match, captures "joe."
lily.example.org                match, captures "lily."
zuck.mark.example.org           match, captures "zuck.mark."
example.org.                    no match
.example.org.                   match, captures ".", but an illegal domain

Benchmarks

The testing environment is running on Ubuntu-16.04-amd64 with i7-7700HQ CPU @ 2.80GHz. Since all test cases are completely copied from file plugin of CoreDNS, so the bench codes are the same as well.

Without DNSSEC

For DnsRouter:

$ go test -v -benchmem -run=^$ github.com/vegertar/dnsrouter -bench ^BenchmarkLookup$

goos: linux
goarch: amd64
pkg: github.com/vegertar/dnsrouter
BenchmarkLookup/DefaultScheme-8         	  300000	      5842 ns/op	    3264 B/op	      57 allocs/op
BenchmarkLookup/SimpleScheme-8          	  500000	      2824 ns/op	    1632 B/op	      29 allocs/op
PASS
ok  	github.com/vegertar/dnsrouter	3.254s
Success: Benchmarks passed.

For file plugin of CoreDNS:

$ go test -benchmem -run=^$ github.com/coredns/coredns/plugin/file -bench ^BenchmarkFileLookup$

goos: linux
goarch: amd64
pkg: github.com/coredns/coredns/plugin/file
BenchmarkFileLookup-8   	  100000	     14265 ns/op	    6243 B/op	      99 allocs/op
PASS
ok  	github.com/coredns/coredns/plugin/file	1.591s
Success: Benchmarks passed.
With DNSSEC

For DnsRouter:

$ go test -v -benchmem -run=^$ github.com/vegertar/dnsrouter -bench ^BenchmarkLookupDNSSEC$

goos: linux
goarch: amd64
pkg: github.com/vegertar/dnsrouter
BenchmarkLookupDNSSEC-8   	  300000	      5605 ns/op	    3312 B/op	      56 allocs/op
PASS
ok  	github.com/vegertar/dnsrouter	1.747s
Success: Benchmarks passed.

For file plugin of CoreDNS:

$ go test -benchmem -run=^$ github.com/coredns/coredns/plugin/file -bench ^BenchmarkFileLookupDNSSEC$

goos: linux
goarch: amd64
pkg: github.com/coredns/coredns/plugin/file
BenchmarkFileLookupDNSSEC-8   	  100000	     21723 ns/op	    9163 B/op	     232 allocs/op
PASS
ok  	github.com/coredns/coredns/plugin/file	2.419s
Success: Benchmarks passed.

TODO

There are some works in planed:

  • NSEC3
  • 100% code coverage
  • Better examples and docs.

Any contributions are welcome.

Documentation

Index

Constants

View Source
const ClassContextKey classContextKeyType = 1

ClassContextKey is used to get Class instance from Request context.

Variables

View Source
var (
	// NoErrorHandler responses dns.RcodeSuccess.
	NoErrorHandler = RcodeHandler(dns.RcodeSuccess)

	// NameErrorHandler responses dns.RcodeNameError.
	NameErrorHandler = RcodeHandler(dns.RcodeNameError)
)
View Source
var (
	// DefaultScheme consists of middlewares serving as a complete stub name server.
	DefaultScheme = []Middleware{
		PanicHandler,
		RefusedHandler,
		OptHandler,
		WildcardHandler,
		NsecHandler,
		NsHandler,
		ExtraHandler,
		CnameHandler,
		BasicHandler,
	}

	// SimpleScheme consists of essential middlewares without filling out AUTHORITY and ADDITIONAL sections.
	// This scheme is faster (2x bench of DefaultScheme) and suitable for most of ordinary situations.
	SimpleScheme = []Middleware{
		PanicHandler,
		RefusedHandler,
		OptHandler,
		WildcardHandler,
		CnameHandler,
		BasicHandler,
	}
)

Functions

func Classic

func Classic(ctx context.Context, h Handler) dns.Handler

Classic converts a Handler into the github.com/miekg/dns.Handler.

func Exists

func Exists(rrSet []dns.RR, t uint16) bool

Exists checks if a given qtype exists.

func ExistsAny

func ExistsAny(rrSet []dns.RR, t ...uint16) bool

ExistsAny checks if any of given qtypes exists.

func First

func First(rrSet []dns.RR, t uint16) int

First returns the index of the first element of given qtype. If not found, returns -1.

func FirstAny

func FirstAny(rrSet []dns.RR, t ...uint16) int

FirstAny returns the index of the first element of any of given qtypes. If not found, returns -1.

func FurtherRequest

func FurtherRequest(w ResponseWriter, req *Request, qname string, qtype uint16, h Handler) dns.Msg

FurtherRequest is a helper function to execute another query within current context.

Types

type Answer

type Answer struct {
	dns.RR
}

Answer is a handler which writes the RR into ANSWER section.

func (Answer) ServeDNS

func (a Answer) ServeDNS(w ResponseWriter, r *Request)

ServeDNS implements Handler interface.

type CheckRedirect

type CheckRedirect interface {
	Qtype() uint16
}

CheckRedirect is useful for checking type assertion on a Handle that returned from a Search if which occurs DNAME or CNAME redirection.

type Class

type Class interface {
	NextSecure(nsecType uint16) (nsec Class)
	Search(qtype uint16) (h Handler)
	Stub() (stub Stub)
	Zone() (zone Class, delegated bool)
}

A Class is acquired from a Stub via an arbitrary name with a class.

type Handler

type Handler interface {
	ServeDNS(ResponseWriter, *Request)
}

A Handler responds to a DNS request.

func BasicHandler

func BasicHandler(h Handler) Handler

BasicHandler is a middleware filling out essential answer section.

func ChainHandler

func ChainHandler(h Handler, middlewares ...Middleware) Handler

ChainHandler applies middlewares on given handler.

func CnameHandler

func CnameHandler(h Handler) Handler

CnameHandler is a middleware following the query on canonical name.

func ExtraHandler

func ExtraHandler(h Handler) Handler

ExtraHandler is a middleware filling out additional A/AAAA records for target names.

func MultiHandler

func MultiHandler(m ...Handler) Handler

MultiHandler merges multiple handlers into a single one.

func NsHandler

func NsHandler(h Handler) Handler

NsHandler returns a middleware that filling out NS section.

func NsecHandler

func NsecHandler(h Handler) Handler

NsecHandler returns a middleware that filling out denial-of-existence records.

func OptHandler

func OptHandler(h Handler) Handler

OptHandler is a middleware filling out OPT records if request is compatible with EDNS0.

func PanicHandler

func PanicHandler(h Handler) Handler

PanicHandler is a middleware filling out an extra TXT record from a recovered panic, as well as setting SERVFAIL.

func ParamsHandler

func ParamsHandler(h Handler, params Params) Handler

ParamsHandler fills the params into request context.

func RefusedHandler

func RefusedHandler(h Handler) Handler

RefusedHandler is a middleware setting REFUSED code if no ANSWERs or NSs either.

func WildcardHandler

func WildcardHandler(h Handler) Handler

WildcardHandler is a middleware expanding wildcard.

type HandlerFunc

type HandlerFunc func(ResponseWriter, *Request)

The HandlerFunc type is an adapter to allow the use of ordinary functions as DNS handlers.

func (HandlerFunc) ServeDNS

func (f HandlerFunc) ServeDNS(w ResponseWriter, r *Request)

ServeDNS implements Handler interface.

type Middleware

type Middleware func(Handler) Handler

Middleware is a piece of middleware.

type Param

type Param struct {
	Key   string
	Value string
}

Param is a single domain parameter, consisting of a key and a value.

type Params

type Params []Param

Params is a Param-slice, as returned by the router. The slice is ordered, the first domain parameter is also the first slice value. It is therefore safe to read values by the index.

func (Params) ByName

func (ps Params) ByName(name string) string

ByName returns the value of the first Param which key matches the given name. If no matching Param is found, an empty string is returned.

type RcodeHandler

type RcodeHandler int

RcodeHandler writes an arbitrary response code.

func (RcodeHandler) ServeDNS

func (e RcodeHandler) ServeDNS(w ResponseWriter, r *Request)

ServeDNS implements Handler interface.

type Request

type Request struct {
	*dns.Msg
	// contains filtered or unexported fields
}

A Request represents a DNS request received by a server.

func NewRequest

func NewRequest(qname string, qtype uint16) *Request

NewRequest makes a question request.

func (*Request) Context

func (r *Request) Context() context.Context

Context returns the request's context. To change the context, use WithContext.

func (*Request) Params

func (r *Request) Params() Params

Params returns the binding params.

func (*Request) WithContext

func (r *Request) WithContext(ctx context.Context) *Request

WithContext returns a shallow copy of r with its context changed to ctx. The provided ctx must be non-nil.

type ResponseWriter

type ResponseWriter interface {
	Msg() *dns.Msg
}

A ResponseWriter interface is used by a DNS handler to construct an DNS response.

func NewResponseWriter

func NewResponseWriter() ResponseWriter

NewResponseWriter creates a response writer.

type Router

type Router struct {

	// Configurable middleware that chaining with the Router.
	// If it is nil, then uses DefaultScheme.
	Middleware []Middleware
	// contains filtered or unexported fields
}

Router is a dns Handler which can be used to dispatch requests to different handler functions via configurable routes.

func New

func New() *Router

New returns a new initialized Router.

func (*Router) Handle

func (r *Router) Handle(s string, handler Handler)

Handle registers a new request handler with a routing pattern, any string that can pass into dns.NewRR is legal to use in here. Only a domain that beginning with a single asterisk (*) is treated as wildcard (https://tools.ietf.org/html/rfc4592), in other cases, wildcard labels or named parameters are treated as same as path components used in httprouter (https://github.com/julienschmidt/httprouter). If the handler is nil then defaults to write the resulted record into answer section. Please pay attention that Handle won't check if the given string contains an actual record data, e.g. "github.com A" is legal to pass to Handle, so calling Handle("github.com A", nil) causes a strange RR "github.com. 3600 IN A " in ANSWER section.

func (*Router) HandleFunc

func (r *Router) HandleFunc(s string, handlerFunc HandlerFunc)

HandleFunc registers a new request handler function with a routing pattern.

func (*Router) HandleZone

func (r *Router) HandleZone(f io.Reader, origin, filename string)

HandleZone loads a zone reader.

func (*Router) HandleZoneFile

func (r *Router) HandleZoneFile(origin, filename string)

HandleZoneFile loads a zone file.

func (*Router) Lookup

func (r *Router) Lookup(name string, qclass uint16) Class

Lookup implements Stub interface, this method would never return nil.

func (*Router) ServeDNS

func (r *Router) ServeDNS(resp ResponseWriter, req *Request)

ServeDNS implements Handler interface.

type Stub

type Stub interface {
	Lookup(name string, qclass uint16) (class Class)
}

A Stub is a name server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL