Add middleware/dnssec (#133)

This adds an online dnssec middleware. The middleware will sign
responses on the fly. Negative responses are signed with NSEC black
lies.
This commit is contained in:
Miek Gieben 2016-04-26 17:57:11 +01:00
parent 8e6c690484
commit 1aa1a92198
39 changed files with 1206 additions and 144 deletions

2
.gitignore vendored
View file

@ -1,2 +1,4 @@
query.log query.log
Corefile Corefile
*.swp
coredns

View file

@ -60,6 +60,7 @@ var directiveOrder = []directive{
{"rewrite", setup.Rewrite}, {"rewrite", setup.Rewrite},
{"loadbalance", setup.Loadbalance}, {"loadbalance", setup.Loadbalance},
{"cache", setup.Cache}, {"cache", setup.Cache},
{"dnssec", setup.Dnssec},
{"file", setup.File}, {"file", setup.File},
{"secondary", setup.Secondary}, {"secondary", setup.Secondary},
{"etcd", setup.Etcd}, {"etcd", setup.Etcd},

View file

@ -27,8 +27,7 @@ func cacheParse(c *Controller) (int, []string, error) {
for c.Next() { for c.Next() {
if c.Val() == "cache" { if c.Val() == "cache" {
// cache [ttl] [zones..] // cache [ttl] [zones..]
origins := c.ServerBlockHosts
origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) > 0 { if len(args) > 0 {
origins = args origins = args
@ -39,7 +38,7 @@ func cacheParse(c *Controller) (int, []string, error) {
origins = origins[1:] origins = origins[1:]
if len(origins) == 0 { if len(origins) == 0 {
// There was *only* the ttl, revert back to server block // There was *only* the ttl, revert back to server block
origins = []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} origins = c.ServerBlockHosts
} }
} }
} }

View file

@ -10,7 +10,7 @@ func TestChaos(t *testing.T) {
tests := []struct { tests := []struct {
input string input string
shouldErr bool shouldErr bool
expectedVersion string // expected veresion. expectedVersion string // expected version.
expectedAuthor string // expected author (string, although we get a map). expectedAuthor string // expected author (string, although we get a map).
expectedErrContent string // substring from the expected error. Empty for positive cases. expectedErrContent string // substring from the expected error. Empty for positive cases.
}{ }{

79
core/setup/dnssec.go Normal file
View file

@ -0,0 +1,79 @@
package setup
import (
"path"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/dnssec"
)
// Dnssec sets up the dnssec middleware.
func Dnssec(c *Controller) (middleware.Middleware, error) {
zones, keys, err := dnssecParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return dnssec.NewDnssec(zones, keys, next)
}, nil
}
func dnssecParse(c *Controller) ([]string, []*dnssec.DNSKEY, error) {
zones := []string{}
keys := []*dnssec.DNSKEY{}
for c.Next() {
if c.Val() == "dnssec" {
// dnssec [zones...]
zones = c.ServerBlockHosts
args := c.RemainingArgs()
if len(args) > 0 {
zones = args
}
for c.NextBlock() {
k, e := keyParse(c)
if e != nil {
// TODO(miek): Log and drop or something? stop startup?
continue
}
keys = append(keys, k...)
}
}
}
for i, _ := range zones {
zones[i] = middleware.Host(zones[i]).Normalize()
}
return zones, keys, nil
}
func keyParse(c *Controller) ([]*dnssec.DNSKEY, error) {
keys := []*dnssec.DNSKEY{}
what := c.Val()
if !c.NextArg() {
return nil, c.ArgErr()
}
value := c.Val()
switch what {
case "key":
if value == "file" {
ks := c.RemainingArgs()
for _, k := range ks {
// Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205
ext := path.Ext(k) // TODO(miek): test things like .key
base := k
if len(ext) > 0 {
base = k[:len(k)-len(ext)]
}
k, err := dnssec.ParseKeyFile(base+".key", base+".private")
if err != nil {
return nil, err
}
keys = append(keys, k)
}
}
}
return keys, nil
}

54
core/setup/dnssec_test.go Normal file
View file

@ -0,0 +1,54 @@
package setup
import (
"strings"
"testing"
)
func TestDnssec(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedZones []string
expectedKeys []string
expectedErrContent string
}{
{
`dnssec`, false, nil, nil, "",
},
{
`dnssec miek.nl`, false, []string{"miek.nl."}, nil, "",
},
}
for i, test := range tests {
c := NewTestController(test.input)
zones, keys, err := dnssecParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErrContent) {
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input)
}
}
if !test.shouldErr {
for i, z := range test.expectedZones {
if zones[i] != z {
t.Errorf("Dnssec not correctly set for input %s. Expected: %s, actual: %s", test.input, z, zones[i])
}
}
for i, k := range test.expectedKeys {
if k != keys[i].K.Header().Name {
t.Errorf("Dnssec not correctly set for input %s. Expected: '%s', actual: '%s'", test.input, k, keys[i].K.Header().Name)
}
}
}
}
}

View file

@ -10,8 +10,8 @@ import (
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/etcd" "github.com/miekg/coredns/middleware/etcd"
"github.com/miekg/coredns/middleware/etcd/singleflight"
"github.com/miekg/coredns/middleware/proxy" "github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/coredns/middleware/singleflight"
etcdc "github.com/coreos/etcd/client" etcdc "github.com/coreos/etcd/client"
"golang.org/x/net/context" "golang.org/x/net/context"

View file

@ -46,7 +46,7 @@ func fileParse(c *Controller) (file.Zones, error) {
} }
fileName := c.Val() fileName := c.Val()
origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} origins := c.ServerBlockHosts
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) > 0 { if len(args) > 0 {
origins = args origins = args
@ -54,7 +54,7 @@ func fileParse(c *Controller) (file.Zones, error) {
reader, err := os.Open(fileName) reader, err := os.Open(fileName)
if err != nil { if err != nil {
return file.Zones{}, err continue
} }
for i, _ := range origins { for i, _ := range origins {
@ -68,7 +68,7 @@ func fileParse(c *Controller) (file.Zones, error) {
noReload := false noReload := false
for c.NextBlock() { for c.NextBlock() {
t, _, e := parseTransfer(c) t, _, e := transferParse(c)
if e != nil { if e != nil {
return file.Zones{}, e return file.Zones{}, e
} }
@ -89,8 +89,8 @@ func fileParse(c *Controller) (file.Zones, error) {
return file.Zones{Z: z, Names: names}, nil return file.Zones{Z: z, Names: names}, nil
} }
// transfer to [address...] // transferParse parses transfer statements: 'transfer to [address...]'.
func parseTransfer(c *Controller) (tos, froms []string, err error) { func transferParse(c *Controller) (tos, froms []string, err error) {
what := c.Val() what := c.Val()
if !c.NextArg() { if !c.NextArg() {
return nil, nil, c.ArgErr() return nil, nil, c.ArgErr()

View file

@ -7,10 +7,7 @@ import (
"github.com/miekg/coredns/middleware/metrics" "github.com/miekg/coredns/middleware/metrics"
) )
const ( const addr = "localhost:9135" // 9153 is occupied by bind_exporter
path = "/metrics"
addr = "localhost:9135" // 9153 is occupied by bind_exporter
)
var once sync.Once var once sync.Once

View file

@ -40,7 +40,7 @@ func secondaryParse(c *Controller) (file.Zones, error) {
for c.Next() { for c.Next() {
if c.Val() == "secondary" { if c.Val() == "secondary" {
// secondary [origin] // secondary [origin]
origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]} origins := c.ServerBlockHosts
args := c.RemainingArgs() args := c.RemainingArgs()
if len(args) > 0 { if len(args) > 0 {
origins = args origins = args
@ -52,7 +52,7 @@ func secondaryParse(c *Controller) (file.Zones, error) {
} }
for c.NextBlock() { for c.NextBlock() {
t, f, e := parseTransfer(c) t, f, e := transferParse(c)
if e != nil { if e != nil {
return file.Zones{}, e return file.Zones{}, e
} }

View file

@ -1,33 +1,5 @@
package cache package cache
/*
The idea behind this implementation is as follows. We have a cache that is index
by a couple different keys, which allows use to have:
- negative cache: qname only for NXDOMAIN responses
- negative cache: qname + qtype for NODATA responses
- positive cache: qname + qtype for succesful responses.
We track DNSSEC responses separately, i.e. under a different cache key.
Each Item stored contains the message split up in the different sections
and a few bits of the msg header.
For instance an NXDOMAIN for blaat.miek.nl will create the
following negative cache entry (do signal state of DO (do off, DO on)).
ncache: do <blaat.miek.nl>
Item:
Ns: <miek.nl> SOA RR
If found a return packet is assembled and returned to the client. Taking size and EDNS0
constraints into account.
We also need to track if the answer received was an authoritative answer, ad bit and other
setting, for this we also store a few header bits.
For the positive cache we use the same idea. Truncated responses are never stored.
*/
import ( import (
"log" "log"
"time" "time"
@ -50,41 +22,7 @@ func NewCache(ttl int, zones []string, next middleware.Handler) Cache {
return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second} return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second}
} }
type messageType int func cacheKey(m *dns.Msg, t middleware.MsgType, do bool) string {
const (
success messageType = iota
nameError // NXDOMAIN in header, SOA in auth.
noData // NOERROR in header, SOA in auth.
otherError // Don't cache these.
)
// classify classifies a message, it returns the MessageType.
func classify(m *dns.Msg) (messageType, *dns.OPT) {
opt := m.IsEdns0()
soa := false
if m.Rcode == dns.RcodeSuccess {
return success, opt
}
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeSOA {
soa = true
break
}
}
// Check length of different section, and drop stuff that is just to large.
if soa && m.Rcode == dns.RcodeSuccess {
return noData, opt
}
if soa && m.Rcode == dns.RcodeNameError {
return nameError, opt
}
return otherError, opt
}
func cacheKey(m *dns.Msg, t messageType, do bool) string {
if m.Truncated { if m.Truncated {
return "" return ""
} }
@ -92,13 +30,15 @@ func cacheKey(m *dns.Msg, t messageType, do bool) string {
qtype := m.Question[0].Qtype qtype := m.Question[0].Qtype
qname := middleware.Name(m.Question[0].Name).Normalize() qname := middleware.Name(m.Question[0].Name).Normalize()
switch t { switch t {
case success: case middleware.Success:
fallthrough
case middleware.Delegation:
return successKey(qname, qtype, do) return successKey(qname, qtype, do)
case nameError: case middleware.NameError:
return nameErrorKey(qname, do) return nameErrorKey(qname, do)
case noData: case middleware.NoData:
return noDataKey(qname, qtype, do) return noDataKey(qname, qtype, do)
case otherError: case middleware.OtherError:
return "" return ""
} }
return "" return ""
@ -116,13 +56,13 @@ func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap tim
func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error {
do := false do := false
mt, opt := classify(res) mt, opt := middleware.Classify(res)
if opt != nil { if opt != nil {
do = opt.Do() do = opt.Do()
} }
key := cacheKey(res, mt, do) key := cacheKey(res, mt, do)
c.Set(res, key, mt) c.set(res, key, mt)
if c.cap != 0 { if c.cap != 0 {
setCap(res, uint32(c.cap.Seconds())) setCap(res, uint32(c.cap.Seconds()))
@ -131,7 +71,7 @@ func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error {
return c.ResponseWriter.WriteMsg(res) return c.ResponseWriter.WriteMsg(res)
} }
func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { func (c *CachingResponseWriter) set(m *dns.Msg, key string, mt middleware.MsgType) {
if key == "" { if key == "" {
// logger the log? TODO(miek) // logger the log? TODO(miek)
return return
@ -139,14 +79,14 @@ func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) {
duration := c.cap duration := c.cap
switch mt { switch mt {
case success: case middleware.Success, middleware.Delegation:
if c.cap == 0 { if c.cap == 0 {
duration = minTtl(m.Answer, mt) duration = minTtl(m.Answer, mt)
} }
i := newItem(m, duration) i := newItem(m, duration)
c.cache.Set(key, i, duration) c.cache.Set(key, i, duration)
case nameError, noData: case middleware.NameError, middleware.NoData:
if c.cap == 0 { if c.cap == 0 {
duration = minTtl(m.Ns, mt) duration = minTtl(m.Ns, mt)
} }
@ -167,19 +107,19 @@ func (c *CachingResponseWriter) Hijack() {
return return
} }
func minTtl(rrs []dns.RR, mt messageType) time.Duration { func minTtl(rrs []dns.RR, mt middleware.MsgType) time.Duration {
if mt != success && mt != nameError && mt != noData { if mt != middleware.Success && mt != middleware.NameError && mt != middleware.NoData {
return 0 return 0
} }
minTtl := maxTtl minTtl := maxTtl
for _, r := range rrs { for _, r := range rrs {
switch mt { switch mt {
case nameError, noData: case middleware.NameError, middleware.NoData:
if r.Header().Rrtype == dns.TypeSOA { if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second return time.Duration(r.(*dns.SOA).Minttl) * time.Second
} }
case success: case middleware.Success, middleware.Delegation:
if r.Header().Ttl < minTtl { if r.Header().Ttl < minTtl {
minTtl = r.Header().Ttl minTtl = r.Header().Ttl
} }

View file

@ -78,13 +78,13 @@ func TestCache(t *testing.T) {
m = cacheMsg(m, tc) m = cacheMsg(m, tc)
do := tc.in.Do do := tc.in.Do
mt, _ := classify(m) mt, _ := middleware.Classify(m)
key := cacheKey(m, mt, do) key := cacheKey(m, mt, do)
crr.Set(m, key, mt) crr.set(m, key, mt)
name := middleware.Name(m.Question[0].Name).Normalize() name := middleware.Name(m.Question[0].Name).Normalize()
qtype := m.Question[0].Qtype qtype := m.Question[0].Qtype
i, ok := c.Get(name, qtype, do) i, ok := c.get(name, qtype, do)
if !ok && !m.Truncated { if !ok && !m.Truncated {
t.Errorf("Truncated message should not have been cached") t.Errorf("Truncated message should not have been cached")
} }

View file

@ -21,7 +21,7 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
do := state.Do() // might need more from OPT record? do := state.Do() // might need more from OPT record?
if i, ok := c.Get(qname, qtype, do); ok { if i, ok := c.get(qname, qtype, do); ok {
resp := i.toMsg(r) resp := i.toMsg(r)
state.SizeAndDo(resp) state.SizeAndDo(resp)
w.WriteMsg(resp) w.WriteMsg(resp)
@ -35,12 +35,13 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
return c.Next.ServeDNS(ctx, crr, r) return c.Next.ServeDNS(ctx, crr, r)
} }
func (c Cache) Get(qname string, qtype uint16, do bool) (*item, bool) { func (c Cache) get(qname string, qtype uint16, do bool) (*item, bool) {
nxdomain := nameErrorKey(qname, do) nxdomain := nameErrorKey(qname, do)
if i, ok := c.cache.Get(nxdomain); ok { if i, ok := c.cache.Get(nxdomain); ok {
return i.(*item), true return i.(*item), true
} }
// TODO(miek): delegation was added double check
successOrNoData := successKey(qname, qtype, do) successOrNoData := successKey(qname, qtype, do)
if i, ok := c.cache.Get(successOrNoData); ok { if i, ok := c.cache.Get(successOrNoData); ok {
return i.(*item), true return i.(*item), true

52
middleware/classify.go Normal file
View file

@ -0,0 +1,52 @@
package middleware
import "github.com/miekg/dns"
type MsgType int
const (
Success MsgType = iota
NameError // NXDOMAIN in header, SOA in auth.
NoData // NOERROR in header, SOA in auth.
Delegation // NOERROR in header, NS in auth, optionally fluff in additional (not checked).
OtherError // Don't cache these.
)
// Classify classifies a message, it returns the MessageType.
func Classify(m *dns.Msg) (MsgType, *dns.OPT) {
opt := m.IsEdns0()
if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess {
return Success, opt
}
soa := false
ns := 0
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeSOA {
soa = true
continue
}
if r.Header().Rrtype == dns.TypeNS {
ns++
}
}
// Check length of different sections, and drop stuff that is just to large? TODO(miek).
if soa && m.Rcode == dns.RcodeSuccess {
return NoData, opt
}
if soa && m.Rcode == dns.RcodeNameError {
return NameError, opt
}
if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess {
return Delegation, opt
}
if m.Rcode == dns.RcodeSuccess {
return Success, opt
}
return OtherError, opt
}

View file

@ -0,0 +1,31 @@
package middleware
import (
"testing"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
func TestClassifyDelegation(t *testing.T) {
m := delegationMsg()
mt, _ := Classify(m)
if mt != Delegation {
t.Errorf("message is wrongly classified, expected delegation, got %d", mt)
}
}
func delegationMsg() *dns.Msg {
return &dns.Msg{
Ns: []dns.RR{
test.NS("miek.nl. 3600 IN NS linode.atoom.net."),
test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."),
test.NS("miek.nl. 3600 IN NS omval.tednet.nl."),
},
Extra: []dns.RR{
test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"),
test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"),
},
}
}

View file

@ -0,0 +1,35 @@
# dnssec
`dnssec` enables on-the-fly DNSSEC signing of served data.
## Syntax
~~~
dnssec [zones...]
~~~
* `zones` zones that should be signed. If empty the zones from the configuration block
are used.
If keys are not specified (see below) a key is generated and used for all signing operations. The
DNSSEC signing will treat this key a CSK (common signing key) forgoing the ZSK/KSK split. All
signing operations are done online. Authenticated denial of existence is implemented with NSEC black
lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to
RSA).
A signing key can be specified by using the `key` directive.
TODO(miek): think about key rollovers.
~~~
dnssec [zones... ] {
key file [key...]
}
~~~
* `key file` indicates key file(s) should be read from disk. When multiple keys are specified, RRset
will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a
ECDSAP256SHA256 <zonename>`. A key created for zone *A* can be safely used for zone *B*.
## Examples

View file

@ -0,0 +1,24 @@
package dnssec
import "github.com/miekg/dns"
// nsec returns an NSEC useful for NXDOMAIN respsones.
// See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00
// For example, a request for the non-existing name a.example.com would
// cause the following NSEC record to be generated:
// a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC )
// This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip
// the header rcode to NOERROR.
func (d Dnssec) nsec(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) {
nsec := &dns.NSEC{}
nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
nsec.NextDomain = "\\000." + name
nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC}
sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir)
if err != nil {
return nil, err
}
return append(sigs, nsec), nil
}

View file

@ -0,0 +1,50 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
func TestZoneSigningBlackLies(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testNxdomainMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Ns, 2) {
t.Errorf("authority section should have 2 sig")
}
var nsec *dns.NSEC
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeNSEC {
nsec = r.(*dns.NSEC)
}
}
if m.Rcode != dns.RcodeSuccess {
t.Errorf("expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode)
}
if nsec == nil {
t.Fatalf("expected NSEC, got none")
}
if nsec.Hdr.Name != "ww.miek.nl." {
t.Errorf("expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name)
}
if nsec.NextDomain != "\\000.ww.miek.nl." {
t.Errorf("expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain)
}
t.Logf("%+v\n", m)
}
func testNxdomainMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
Question: []dns.Question{dns.Question{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}},
Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")},
}
}

View file

@ -0,0 +1,23 @@
package dnssec
import (
"hash/fnv"
"strconv"
"github.com/miekg/dns"
)
// Key serializes the RRset and return a signature cache key.
func key(rrs []dns.RR) string {
h := fnv.New64()
buf := make([]byte, 256)
for _, r := range rrs {
off, err := dns.PackRR(r, buf, 0, nil, false)
if err == nil {
h.Write(buf[:off])
}
}
i := h.Sum64()
return strconv.FormatUint(i, 10)
}

View file

@ -0,0 +1,32 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
)
func TestCacheSet(t *testing.T) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
defer rmPriv()
defer rmPub()
dnskey, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
m := testMsg()
state := middleware.State{Req: m}
k := key(m.Answer) // calculate *before* we add the sig
d := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil)
m = d.Sign(state, "miek.nl.", time.Now().UTC())
_, ok := d.get(k)
if !ok {
t.Errorf("signature was not added to the cache")
}
}

View file

@ -0,0 +1,71 @@
package dnssec
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"errors"
"os"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
)
type DNSKEY struct {
K *dns.DNSKEY
s crypto.Signer
keytag uint16
}
// ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other
// utilities. It adds ".key" for the public key and ".private" for the private key.
func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) {
f, e := os.Open(pubFile)
if e != nil {
return nil, e
}
k, e := dns.ReadRR(f, pubFile)
if e != nil {
return nil, e
}
f, e = os.Open(privFile)
if e != nil {
return nil, e
}
p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile)
if e != nil {
return nil, e
}
if v, ok := p.(*rsa.PrivateKey); ok {
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
}
if v, ok := p.(*ecdsa.PrivateKey); ok {
return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil
}
return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? private key found")
}
// getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true.
func (d Dnssec) getDNSKEY(state middleware.State, zone string, do bool) *dns.Msg {
keys := make([]dns.RR, len(d.keys))
for i, k := range d.keys {
keys[i] = dns.Copy(k.K)
keys[i].Header().Name = zone
}
m := new(dns.Msg)
m.SetReply(state.Req)
m.Answer = keys
if !do {
return m
}
incep, expir := incepExpir(time.Now().UTC())
if sigs, err := d.sign(keys, zone, 3600, incep, expir); err == nil {
m.Answer = append(m.Answer, sigs...)
}
return m
}

127
middleware/dnssec/dnssec.go Normal file
View file

@ -0,0 +1,127 @@
package dnssec
import (
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/singleflight"
"github.com/miekg/dns"
gcache "github.com/patrickmn/go-cache"
)
type Dnssec struct {
Next middleware.Handler
zones []string
keys []*DNSKEY
inflight *singleflight.Group
cache *gcache.Cache
}
func NewDnssec(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec {
return Dnssec{Next: next,
zones: zones,
keys: keys,
cache: gcache.New(defaultDuration, purgeDuration),
inflight: new(singleflight.Group),
}
}
// Sign signs the message m. it takes care of negative or nodata responses. It
// uses NSEC black lies for authenticated denial of existence. Signatures
// creates will be cached for a short while. By default we sign for 8 days,
// starting 3 hours ago.
func (d Dnssec) Sign(state middleware.State, zone string, now time.Time) *dns.Msg {
req := state.Req
mt, _ := middleware.Classify(req) // TODO(miek): need opt record here?
if mt == middleware.Delegation {
return req
}
incep, expir := incepExpir(now)
if mt == middleware.NameError {
if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 {
return req
}
ttl := req.Ns[0].Header().Ttl
if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode
req.Rcode = dns.RcodeSuccess
}
return req
}
for _, r := range rrSets(req.Answer) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Answer = append(req.Answer, sigs...)
}
}
for _, r := range rrSets(req.Ns) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
}
for _, r := range rrSets(req.Extra) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
req.Extra = append(req.Extra, sigs...)
}
}
return req
}
func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) {
k := key(rrs)
sgs, ok := d.get(k)
if ok {
return sgs, nil
}
sigs, err := d.inflight.Do(k, func() (interface{}, error) {
sigs := make([]dns.RR, len(d.keys))
var e error
for i, k := range d.keys {
sig := k.NewRRSIG(signerName, ttl, incep, expir)
e = sig.Sign(k.s, rrs)
sigs[i] = sig
}
d.set(k, sigs)
return sigs, e
})
return sigs.([]dns.RR), err
}
func (d Dnssec) set(key string, sigs []dns.RR) {
// we insert the sigs with a duration that is 24 hours less then the expiration, as these
// sigs have *just* been made the duration is 7 days.
d.cache.Set(key, sigs, eightDays-24*time.Hour)
}
func (d Dnssec) get(key string) ([]dns.RR, bool) {
if s, ok := d.cache.Get(key); ok {
return s.([]dns.RR), true
}
return nil, false
}
func incepExpir(now time.Time) (uint32, uint32) {
incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such
expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days
return incep, expir
}
const (
purgeDuration = 3 * time.Hour
defaultDuration = 24 * time.Hour
eightDays = 8 * 24 * time.Hour
)

View file

@ -0,0 +1,193 @@
package dnssec
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
func TestZoneSigning(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 1) {
t.Errorf("authority section should have 1 sig")
}
}
func TestZoneSigningDouble(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
fPriv1, rmPriv1, _ := test.TempFile(t, ".", privKey1)
fPub1, rmPub1, _ := test.TempFile(t, ".", pubKey1)
defer rmPriv1()
defer rmPub1()
key1, err := ParseKeyFile(fPub1, fPriv1)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
d.keys = append(d.keys, key1)
m := testMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 2) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 2) {
t.Errorf("authority section should have 1 sig")
}
t.Logf("%+v\n", m)
}
// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org.
func TestSigningDifferentZone(t *testing.T) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
defer rmPriv()
defer rmPub()
key, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
m := testMsgEx()
state := middleware.State{Req: m}
d := NewDnssec([]string{"example.org."}, []*DNSKEY{key}, nil)
m = d.Sign(state, "example.org.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
if !section(m.Ns, 1) {
t.Errorf("authority section should have 1 sig")
}
t.Logf("%+v\n", m)
}
func TestSigningCname(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsgCname()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
t.Logf("%+v\n", m)
}
func TestZoneSigningDelegation(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testDelegationMsg()
state := middleware.State{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Ns, 0) {
t.Errorf("authority section should have 0 sig")
t.Logf("%v\n", m)
}
if !section(m.Extra, 0) {
t.Errorf("answer section should have 0 sig")
t.Logf("%v\n", m)
}
}
func section(rss []dns.RR, nrSigs int) bool {
i := 0
for _, r := range rss {
if r.Header().Rrtype == dns.TypeRRSIG {
i++
}
}
return nrSigs == i
}
func testMsg() *dns.Msg {
// don't care about the message header
return &dns.Msg{
Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")},
Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")},
}
}
func testMsgEx() *dns.Msg {
return &dns.Msg{
Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")},
Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")},
}
}
func testMsgCname() *dns.Msg {
return &dns.Msg{
Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")},
}
}
func testDelegationMsg() *dns.Msg {
return &dns.Msg{
Ns: []dns.RR{
test.NS("miek.nl. 3600 IN NS linode.atoom.net."),
test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."),
test.NS("miek.nl. 3600 IN NS omval.tednet.nl."),
},
Extra: []dns.RR{
test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"),
test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"),
},
}
}
func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
k, rm1, rm2 := newKey(t)
d := NewDnssec(zones, []*DNSKEY{k}, nil)
return d, rm1, rm2
}
func newKey(t *testing.T) (*DNSKEY, func(), func()) {
fPriv, rmPriv, _ := test.TempFile(t, ".", privKey)
fPub, rmPub, _ := test.TempFile(t, ".", pubKey)
key, err := ParseKeyFile(fPub, fPriv)
if err != nil {
t.Fatalf("failed to parse key: %v\n", err)
}
return key, rmPriv, rmPub
}
const (
pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==`
privKey = `Private-key-format: v1.3
Algorithm: 13 (ECDSAP256SHA256)
PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs=
Created: 20160423195532
Publish: 20160423195532
Activate: 20160423195532
`
pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==`
privKey1 = `Private-key-format: v1.3
Algorithm: 13 (ECDSAP256SHA256)
PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c=
Created: 20160423211746
Publish: 20160423211746
Activate: 20160423211746
`
)

View file

@ -0,0 +1,61 @@
package dnssec
import (
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
// ServeDNS implements the middleware.Handler interface.
func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := middleware.State{W: w, Req: r}
do := state.Do()
qname := state.Name()
qtype := state.QType()
zone := middleware.Zones(d.zones).Matches(qname)
if zone == "" {
return d.Next.ServeDNS(ctx, w, r)
}
// Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let
// the query through.
if qtype == dns.TypeDNSKEY {
for _, z := range d.zones {
if qname == z {
resp := d.getDNSKEY(state, z, do)
state.SizeAndDo(resp)
w.WriteMsg(resp)
return dns.RcodeSuccess, nil
}
}
}
drr := NewDnssecResponseWriter(w, d)
return d.Next.ServeDNS(ctx, drr, r)
}
var (
cacheHitCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: middleware.Namespace,
Subsystem: subsystem,
Name: "hit_count_total",
Help: "Counter of signatures that were found in the cache.",
}, []string{"zone"})
cacheMissCount = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: middleware.Namespace,
Subsystem: subsystem,
Name: "miss_count_total",
Help: "Counter of signatures that were not found in the cache.",
}, []string{"zone"})
)
const subsystem = "dnssec"
func init() {
prometheus.MustRegister(cacheHitCount)
prometheus.MustRegister(cacheMissCount)
}

View file

@ -0,0 +1,170 @@
package dnssec
import (
"sort"
"strings"
"testing"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/file"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
var dnssecTestCases = []test.Case{
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
}
var dnsTestCases = []test.Case{
{
Qname: "miek.nl.", Qtype: dns.TypeDNSKEY,
Answer: []dns.RR{
test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true,
Answer: []dns.RR{
test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"),
test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true,
Rcode: dns.RcodeServerFailure,
// Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS.
},
}
func TestLookupZone(t *testing.T) {
zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin")
if err != nil {
return
}
fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}}
dnskey, rm1, rm2 := newKey(t)
defer rm1()
defer rm2()
dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm)
ctx := context.TODO()
for _, tc := range dnsTestCases {
m := tc.Msg()
rec := middleware.NewResponseRecorder(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg()
sort.Sort(test.RRSet(resp.Answer))
sort.Sort(test.RRSet(resp.Ns))
sort.Sort(test.RRSet(resp.Extra))
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
func TestLookupDNSKEY(t *testing.T) {
dnskey, rm1, rm2 := newKey(t)
defer rm1()
defer rm2()
dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler())
ctx := context.TODO()
for _, tc := range dnssecTestCases {
m := tc.Msg()
rec := middleware.NewResponseRecorder(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg()
sort.Sort(test.RRSet(resp.Answer))
sort.Sort(test.RRSet(resp.Ns))
sort.Sort(test.RRSet(resp.Extra))
if !test.Header(t, tc, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
const dbMiekNL = `
$TTL 30M
$ORIGIN miek.nl.
@ IN SOA linode.atoom.net. miek.miek.nl. (
1282630057 ; Serial
4H ; Refresh
1H ; Retry
7D ; Expire
4H ) ; Negative Cache TTL
IN NS linode.atoom.net.
IN MX 1 aspmx.l.google.com.
IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
a IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
www IN CNAME a`

View file

@ -0,0 +1,48 @@
package dnssec
import (
"log"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
)
type DnssecResponseWriter struct {
dns.ResponseWriter
d Dnssec
}
func NewDnssecResponseWriter(w dns.ResponseWriter, d Dnssec) *DnssecResponseWriter {
return &DnssecResponseWriter{w, d}
}
func (d *DnssecResponseWriter) WriteMsg(res *dns.Msg) error {
// By definition we should sign anything that comes back, we should still figure out for
// which zone it should be.
state := middleware.State{W: d.ResponseWriter, Req: res}
qname := state.Name()
zone := middleware.Zones(d.d.zones).Matches(qname)
if zone == "" {
return d.ResponseWriter.WriteMsg(res)
}
if state.Do() {
res = d.d.Sign(state, zone, time.Now().UTC())
}
state.SizeAndDo(res)
return d.ResponseWriter.WriteMsg(res)
}
func (d *DnssecResponseWriter) Write(buf []byte) (int, error) {
log.Printf("[WARNING] Dnssec called with Write: not signing reply")
n, err := d.ResponseWriter.Write(buf)
return n, err
}
func (d *DnssecResponseWriter) Hijack() {
d.ResponseWriter.Hijack()
return
}

View file

@ -0,0 +1,53 @@
package dnssec
import "github.com/miekg/dns"
// newRRSIG return a new RRSIG, with all fields filled out, except the signed data.
func (k *DNSKEY) NewRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG {
sig := new(dns.RRSIG)
sig.Hdr.Rrtype = dns.TypeRRSIG
sig.Algorithm = k.K.Algorithm
sig.KeyTag = k.keytag
sig.SignerName = signerName
sig.Hdr.Ttl = ttl
sig.OrigTtl = origTtl
sig.Inception = incep
sig.Expiration = expir
return sig
}
type rrset struct {
qname string
qtype uint16
}
// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed.
func rrSets(rrs []dns.RR) map[rrset][]dns.RR {
m := make(map[rrset][]dns.RR)
for _, r := range rrs {
if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT {
continue
}
if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok {
s = append(s, r)
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
continue
}
s := make([]dns.RR, 1, 3)
s[0] = r
m[rrset{r.Header().Name, r.Header().Rrtype}] = s
}
if len(m) > 0 {
return m
}
return nil
}
const origTtl = 3600

View file

@ -8,8 +8,8 @@ import (
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/etcd/msg" "github.com/miekg/coredns/middleware/etcd/msg"
"github.com/miekg/coredns/middleware/etcd/singleflight"
"github.com/miekg/coredns/middleware/proxy" "github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/coredns/middleware/singleflight"
etcdc "github.com/coreos/etcd/client" etcdc "github.com/coreos/etcd/client"
"golang.org/x/net/context" "golang.org/x/net/context"

View file

@ -317,11 +317,14 @@ func (e Etcd) NS(zone string, state middleware.State) (records, extra []dns.RR,
// NS record for this zone live in a special place, ns.dns.<zone>. Fake our lookup. // NS record for this zone live in a special place, ns.dns.<zone>. Fake our lookup.
// only a tad bit fishy... // only a tad bit fishy...
old := state.QName() old := state.QName()
state.Clear()
state.Req.Question[0].Name = "ns.dns." + zone state.Req.Question[0].Name = "ns.dns." + zone
services, err := e.records(state, false) services, err := e.records(state, false)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
// ... and reset
state.Req.Question[0].Name = old state.Req.Question[0].Name = old
for _, serv := range services { for _, serv := range services {

View file

@ -10,8 +10,8 @@ import (
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/etcd/msg" "github.com/miekg/coredns/middleware/etcd/msg"
"github.com/miekg/coredns/middleware/etcd/singleflight"
"github.com/miekg/coredns/middleware/proxy" "github.com/miekg/coredns/middleware/proxy"
"github.com/miekg/coredns/middleware/singleflight"
"github.com/miekg/coredns/middleware/test" "github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns" "github.com/miekg/dns"

View file

@ -52,6 +52,10 @@ func (z *Zone) nameErrorProof(qname string, qtype uint16) []dns.RR {
} }
} }
if len(nsec) == 0 || len(nsec1) == 0 {
return nsec
}
// Check for duplicate NSEC. // Check for duplicate NSEC.
if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name && if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name &&
nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain { nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain {

View file

@ -1,7 +1,7 @@
package file package file
import ( import (
"fmt" "errors"
"io" "io"
"log" "log"
@ -27,12 +27,15 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
state := middleware.State{W: w, Req: r} state := middleware.State{W: w, Req: r}
if state.QClass() != dns.ClassINET { if state.QClass() != dns.ClassINET {
return dns.RcodeServerFailure, fmt.Errorf("can only deal with ClassINET") return dns.RcodeServerFailure, errors.New("can only deal with ClassINET")
} }
qname := state.Name() qname := state.Name()
zone := middleware.Zones(f.Zones.Names).Matches(qname) zone := middleware.Zones(f.Zones.Names).Matches(qname)
if zone == "" { if zone == "" {
return f.Next.ServeDNS(ctx, w, r) if f.Next != nil {
return f.Next.ServeDNS(ctx, w, r)
}
return dns.RcodeServerFailure, errors.New("no next middleware found")
} }
z, ok := f.Zones.Z[zone] z, ok := f.Zones.Z[zone]
if !ok { if !ok {

View file

@ -11,7 +11,7 @@ import (
) )
func TestZoneReload(t *testing.T) { func TestZoneReload(t *testing.T) {
fileName, rm, err := test.Zone(t, ".", reloadZoneTest) fileName, rm, err := test.TempFile(t, ".", reloadZoneTest)
if err != nil { if err != nil {
t.Fatalf("failed to create zone: %s", err) t.Fatalf("failed to create zone: %s", err)
} }

View file

@ -15,9 +15,13 @@ type State struct {
Req *dns.Msg Req *dns.Msg
W dns.ResponseWriter W dns.ResponseWriter
// Cache size after first call to Size or Do // Cache size after first call to Size or Do.
size int size int
do int // 0: not, 1: true: 2: false do int // 0: not, 1: true: 2: false
// TODO(miek): opt record itself as well.
// Cache name as (lowercase) well
name string
} }
// Now returns the current timestamp in the specified format. // Now returns the current timestamp in the specified format.
@ -26,12 +30,6 @@ func (s *State) Now(format string) string { return time.Now().Format(format) }
// NowDate returns the current date/time that can be used in other time functions. // NowDate returns the current date/time that can be used in other time functions.
func (s *State) NowDate() time.Time { return time.Now() } func (s *State) NowDate() time.Time { return time.Now() }
// Header gets the heaser of the request in State.
func (s *State) Header() *dns.RR_Header {
// TODO(miek)
return nil
}
// IP gets the (remote) IP address of the client making the request. // IP gets the (remote) IP address of the client making the request.
func (s *State) IP() string { func (s *State) IP() string {
ip, _, err := net.SplitHostPort(s.W.RemoteAddr().String()) ip, _, err := net.SplitHostPort(s.W.RemoteAddr().String())
@ -191,7 +189,13 @@ func (s *State) QType() uint16 { return s.Req.Question[0].Qtype }
// Name returns the name of the question in the request. Note // Name returns the name of the question in the request. Note
// this name will always have a closing dot and will be lower cased. // this name will always have a closing dot and will be lower cased.
func (s *State) Name() string { return strings.ToLower(dns.Name(s.Req.Question[0].Name).String()) } func (s *State) Name() string {
if s.name != "" {
return s.name
}
s.name = strings.ToLower(dns.Name(s.Req.Question[0].Name).String())
return s.name
}
// QName returns the name of the question in the request. // QName returns the name of the question in the request.
func (s *State) QName() string { return dns.Name(s.Req.Question[0].Name).String() } func (s *State) QName() string { return dns.Name(s.Req.Question[0].Name).String() }
@ -210,6 +214,11 @@ func (s *State) ErrorMessage(rcode int) *dns.Msg {
return m return m
} }
// Clear clears all caching from State s.
func (s *State) Clear() {
s.name = ""
}
const ( const (
doTrue = 1 doTrue = 1
doFalse = 2 doFalse = 2

20
middleware/test/file.go Normal file
View file

@ -0,0 +1,20 @@
package test
import (
"io/ioutil"
"os"
"testing"
)
// TempFile will create a temporary file on disk and returns the name and a cleanup function to remove it later.
func TempFile(t *testing.T, dir, content string) (string, func(), error) {
f, err := ioutil.TempFile(dir, "go-test-tmpfile")
if err != nil {
return "", nil, err
}
if err := ioutil.WriteFile(f.Name(), []byte(content), 0644); err != nil {
return "", nil, err
}
rmFunc := func() { os.Remove(f.Name()) }
return f.Name(), rmFunc, nil
}

View file

@ -45,17 +45,18 @@ func (c Case) Msg() *dns.Msg {
return m return m
} }
func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) }
func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) }
func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) }
func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) }
func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) }
func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) }
func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) }
func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) }
func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) }
func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) }
func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) }
func DNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) }
func OPT(bufsize int, do bool) *dns.OPT { func OPT(bufsize int, do bool) *dns.OPT {
o := new(dns.OPT) o := new(dns.OPT)

View file

@ -1,21 +0,0 @@
package test
import (
"io/ioutil"
"os"
"testing"
)
// Zone will create a temporary file on disk and returns the name and
// cleanup function to remove it later.
func Zone(t *testing.T, dir, zonefile string) (string, func(), error) {
f, err := ioutil.TempFile(dir, "go-test-zone")
if err != nil {
return "", nil, err
}
if err := ioutil.WriteFile(f.Name(), []byte(zonefile), 0644); err != nil {
return "", nil, err
}
rmFunc := func() { os.Remove(f.Name()) }
return f.Name(), rmFunc, nil
}

View file

@ -21,7 +21,7 @@ example.org. IN A 127.0.0.1
` `
func TestLookupProxy(t *testing.T) { func TestLookupProxy(t *testing.T) {
name, rm, err := test.Zone(t, ".", exampleOrg) name, rm, err := test.TempFile(t, ".", exampleOrg)
if err != nil { if err != nil {
t.Fatalf("failed to created zone: %s", err) t.Fatalf("failed to created zone: %s", err)
} }