aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Miek Gieben <miek@miek.nl> 2016-04-26 17:57:11 +0100
committerGravatar Miek Gieben <miek@miek.nl> 2016-04-26 17:57:11 +0100
commit1aa1a9219825e9f203eff718cb85360df706e542 (patch)
tree5a046639aeae5e1c2e3b6a62c5f19f7231de3cc6
parent8e6c690484e3bab2dbebcf1958bfef168be6b922 (diff)
downloadcoredns-1aa1a9219825e9f203eff718cb85360df706e542.tar.gz
coredns-1aa1a9219825e9f203eff718cb85360df706e542.tar.zst
coredns-1aa1a9219825e9f203eff718cb85360df706e542.zip
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.
-rw-r--r--.gitignore2
-rw-r--r--core/directives.go1
-rw-r--r--core/setup/cache.go5
-rw-r--r--core/setup/chaos_test.go2
-rw-r--r--core/setup/dnssec.go79
-rw-r--r--core/setup/dnssec_test.go54
-rw-r--r--core/setup/etcd.go2
-rw-r--r--core/setup/file.go10
-rw-r--r--core/setup/metrics.go5
-rw-r--r--core/setup/secondary.go4
-rw-r--r--middleware/cache/cache.go92
-rw-r--r--middleware/cache/cache_test.go6
-rw-r--r--middleware/cache/handler.go5
-rw-r--r--middleware/classify.go52
-rw-r--r--middleware/classify_test.go31
-rw-r--r--middleware/dnssec/README.md35
-rw-r--r--middleware/dnssec/black_lies.go24
-rw-r--r--middleware/dnssec/black_lies_test.go50
-rw-r--r--middleware/dnssec/cache.go23
-rw-r--r--middleware/dnssec/cache_test.go32
-rw-r--r--middleware/dnssec/dnskey.go71
-rw-r--r--middleware/dnssec/dnssec.go127
-rw-r--r--middleware/dnssec/dnssec_test.go193
-rw-r--r--middleware/dnssec/handler.go61
-rw-r--r--middleware/dnssec/handler_test.go170
-rw-r--r--middleware/dnssec/responsewriter.go48
-rw-r--r--middleware/dnssec/rrsig.go53
-rw-r--r--middleware/etcd/etcd.go2
-rw-r--r--middleware/etcd/lookup.go3
-rw-r--r--middleware/etcd/setup_test.go2
-rw-r--r--middleware/file/closest.go4
-rw-r--r--middleware/file/file.go9
-rw-r--r--middleware/file/reload_test.go2
-rw-r--r--middleware/singleflight/singleflight.go (renamed from middleware/etcd/singleflight/singleflight.go)0
-rw-r--r--middleware/state.go25
-rw-r--r--middleware/test/file.go20
-rw-r--r--middleware/test/helpers.go23
-rw-r--r--middleware/test/zone.go21
-rw-r--r--test/proxy_test.go2
39 files changed, 1206 insertions, 144 deletions
diff --git a/.gitignore b/.gitignore
index 486c8230f..fe121cbce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
query.log
Corefile
+*.swp
+coredns
diff --git a/core/directives.go b/core/directives.go
index c6cb81592..862e87b68 100644
--- a/core/directives.go
+++ b/core/directives.go
@@ -60,6 +60,7 @@ var directiveOrder = []directive{
{"rewrite", setup.Rewrite},
{"loadbalance", setup.Loadbalance},
{"cache", setup.Cache},
+ {"dnssec", setup.Dnssec},
{"file", setup.File},
{"secondary", setup.Secondary},
{"etcd", setup.Etcd},
diff --git a/core/setup/cache.go b/core/setup/cache.go
index 0d2440cfc..f5a1cf0d9 100644
--- a/core/setup/cache.go
+++ b/core/setup/cache.go
@@ -27,8 +27,7 @@ func cacheParse(c *Controller) (int, []string, error) {
for c.Next() {
if c.Val() == "cache" {
// cache [ttl] [zones..]
-
- origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
+ origins := c.ServerBlockHosts
args := c.RemainingArgs()
if len(args) > 0 {
origins = args
@@ -39,7 +38,7 @@ func cacheParse(c *Controller) (int, []string, error) {
origins = origins[1:]
if len(origins) == 0 {
// There was *only* the ttl, revert back to server block
- origins = []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
+ origins = c.ServerBlockHosts
}
}
}
diff --git a/core/setup/chaos_test.go b/core/setup/chaos_test.go
index a745545fc..8431cecef 100644
--- a/core/setup/chaos_test.go
+++ b/core/setup/chaos_test.go
@@ -10,7 +10,7 @@ func TestChaos(t *testing.T) {
tests := []struct {
input string
shouldErr bool
- expectedVersion string // expected veresion.
+ expectedVersion string // expected version.
expectedAuthor string // expected author (string, although we get a map).
expectedErrContent string // substring from the expected error. Empty for positive cases.
}{
diff --git a/core/setup/dnssec.go b/core/setup/dnssec.go
new file mode 100644
index 000000000..523f6e659
--- /dev/null
+++ b/core/setup/dnssec.go
@@ -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
+}
diff --git a/core/setup/dnssec_test.go b/core/setup/dnssec_test.go
new file mode 100644
index 000000000..364a363bd
--- /dev/null
+++ b/core/setup/dnssec_test.go
@@ -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)
+ }
+ }
+ }
+ }
+}
diff --git a/core/setup/etcd.go b/core/setup/etcd.go
index 228829008..8e7740e12 100644
--- a/core/setup/etcd.go
+++ b/core/setup/etcd.go
@@ -10,8 +10,8 @@ import (
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/etcd"
- "github.com/miekg/coredns/middleware/etcd/singleflight"
"github.com/miekg/coredns/middleware/proxy"
+ "github.com/miekg/coredns/middleware/singleflight"
etcdc "github.com/coreos/etcd/client"
"golang.org/x/net/context"
diff --git a/core/setup/file.go b/core/setup/file.go
index 0b33fe47f..b535332a7 100644
--- a/core/setup/file.go
+++ b/core/setup/file.go
@@ -46,7 +46,7 @@ func fileParse(c *Controller) (file.Zones, error) {
}
fileName := c.Val()
- origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
+ origins := c.ServerBlockHosts
args := c.RemainingArgs()
if len(args) > 0 {
origins = args
@@ -54,7 +54,7 @@ func fileParse(c *Controller) (file.Zones, error) {
reader, err := os.Open(fileName)
if err != nil {
- return file.Zones{}, err
+ continue
}
for i, _ := range origins {
@@ -68,7 +68,7 @@ func fileParse(c *Controller) (file.Zones, error) {
noReload := false
for c.NextBlock() {
- t, _, e := parseTransfer(c)
+ t, _, e := transferParse(c)
if e != nil {
return file.Zones{}, e
}
@@ -89,8 +89,8 @@ func fileParse(c *Controller) (file.Zones, error) {
return file.Zones{Z: z, Names: names}, nil
}
-// transfer to [address...]
-func parseTransfer(c *Controller) (tos, froms []string, err error) {
+// transferParse parses transfer statements: 'transfer to [address...]'.
+func transferParse(c *Controller) (tos, froms []string, err error) {
what := c.Val()
if !c.NextArg() {
return nil, nil, c.ArgErr()
diff --git a/core/setup/metrics.go b/core/setup/metrics.go
index 84fdadb29..262550a90 100644
--- a/core/setup/metrics.go
+++ b/core/setup/metrics.go
@@ -7,10 +7,7 @@ import (
"github.com/miekg/coredns/middleware/metrics"
)
-const (
- path = "/metrics"
- addr = "localhost:9135" // 9153 is occupied by bind_exporter
-)
+const addr = "localhost:9135" // 9153 is occupied by bind_exporter
var once sync.Once
diff --git a/core/setup/secondary.go b/core/setup/secondary.go
index 0abf82c04..e1f54a651 100644
--- a/core/setup/secondary.go
+++ b/core/setup/secondary.go
@@ -40,7 +40,7 @@ func secondaryParse(c *Controller) (file.Zones, error) {
for c.Next() {
if c.Val() == "secondary" {
// secondary [origin]
- origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
+ origins := c.ServerBlockHosts
args := c.RemainingArgs()
if len(args) > 0 {
origins = args
@@ -52,7 +52,7 @@ func secondaryParse(c *Controller) (file.Zones, error) {
}
for c.NextBlock() {
- t, f, e := parseTransfer(c)
+ t, f, e := transferParse(c)
if e != nil {
return file.Zones{}, e
}
diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go
index 1ec71b047..71df68fdd 100644
--- a/middleware/cache/cache.go
+++ b/middleware/cache/cache.go
@@ -1,33 +1,5 @@
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 (
"log"
"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}
}
-type messageType int
-
-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 {
+func cacheKey(m *dns.Msg, t middleware.MsgType, do bool) string {
if m.Truncated {
return ""
}
@@ -92,13 +30,15 @@ func cacheKey(m *dns.Msg, t messageType, do bool) string {
qtype := m.Question[0].Qtype
qname := middleware.Name(m.Question[0].Name).Normalize()
switch t {
- case success:
+ case middleware.Success:
+ fallthrough
+ case middleware.Delegation:
return successKey(qname, qtype, do)
- case nameError:
+ case middleware.NameError:
return nameErrorKey(qname, do)
- case noData:
+ case middleware.NoData:
return noDataKey(qname, qtype, do)
- case otherError:
+ case middleware.OtherError:
return ""
}
return ""
@@ -116,13 +56,13 @@ func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap tim
func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error {
do := false
- mt, opt := classify(res)
+ mt, opt := middleware.Classify(res)
if opt != nil {
do = opt.Do()
}
key := cacheKey(res, mt, do)
- c.Set(res, key, mt)
+ c.set(res, key, mt)
if c.cap != 0 {
setCap(res, uint32(c.cap.Seconds()))
@@ -131,7 +71,7 @@ func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error {
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 == "" {
// logger the log? TODO(miek)
return
@@ -139,14 +79,14 @@ func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) {
duration := c.cap
switch mt {
- case success:
+ case middleware.Success, middleware.Delegation:
if c.cap == 0 {
duration = minTtl(m.Answer, mt)
}
i := newItem(m, duration)
c.cache.Set(key, i, duration)
- case nameError, noData:
+ case middleware.NameError, middleware.NoData:
if c.cap == 0 {
duration = minTtl(m.Ns, mt)
}
@@ -167,19 +107,19 @@ func (c *CachingResponseWriter) Hijack() {
return
}
-func minTtl(rrs []dns.RR, mt messageType) time.Duration {
- if mt != success && mt != nameError && mt != noData {
+func minTtl(rrs []dns.RR, mt middleware.MsgType) time.Duration {
+ if mt != middleware.Success && mt != middleware.NameError && mt != middleware.NoData {
return 0
}
minTtl := maxTtl
for _, r := range rrs {
switch mt {
- case nameError, noData:
+ case middleware.NameError, middleware.NoData:
if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
}
- case success:
+ case middleware.Success, middleware.Delegation:
if r.Header().Ttl < minTtl {
minTtl = r.Header().Ttl
}
diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go
index 310a1164e..452831082 100644
--- a/middleware/cache/cache_test.go
+++ b/middleware/cache/cache_test.go
@@ -78,13 +78,13 @@ func TestCache(t *testing.T) {
m = cacheMsg(m, tc)
do := tc.in.Do
- mt, _ := classify(m)
+ mt, _ := middleware.Classify(m)
key := cacheKey(m, mt, do)
- crr.Set(m, key, mt)
+ crr.set(m, key, mt)
name := middleware.Name(m.Question[0].Name).Normalize()
qtype := m.Question[0].Qtype
- i, ok := c.Get(name, qtype, do)
+ i, ok := c.get(name, qtype, do)
if !ok && !m.Truncated {
t.Errorf("Truncated message should not have been cached")
}
diff --git a/middleware/cache/handler.go b/middleware/cache/handler.go
index 35443fa1d..b891d7278 100644
--- a/middleware/cache/handler.go
+++ b/middleware/cache/handler.go
@@ -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?
- if i, ok := c.Get(qname, qtype, do); ok {
+ if i, ok := c.get(qname, qtype, do); ok {
resp := i.toMsg(r)
state.SizeAndDo(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)
}
-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)
if i, ok := c.cache.Get(nxdomain); ok {
return i.(*item), true
}
+ // TODO(miek): delegation was added double check
successOrNoData := successKey(qname, qtype, do)
if i, ok := c.cache.Get(successOrNoData); ok {
return i.(*item), true
diff --git a/middleware/classify.go b/middleware/classify.go
new file mode 100644
index 000000000..72c131157
--- /dev/null
+++ b/middleware/classify.go
@@ -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
+}
diff --git a/middleware/classify_test.go b/middleware/classify_test.go
new file mode 100644
index 000000000..26c52db55
--- /dev/null
+++ b/middleware/classify_test.go
@@ -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"),
+ },
+ }
+}
diff --git a/middleware/dnssec/README.md b/middleware/dnssec/README.md
new file mode 100644
index 000000000..df00866cf
--- /dev/null
+++ b/middleware/dnssec/README.md
@@ -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
diff --git a/middleware/dnssec/black_lies.go b/middleware/dnssec/black_lies.go
new file mode 100644
index 000000000..527b2fc3e
--- /dev/null
+++ b/middleware/dnssec/black_lies.go
@@ -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
+}
diff --git a/middleware/dnssec/black_lies_test.go b/middleware/dnssec/black_lies_test.go
new file mode 100644
index 000000000..951e8952e
--- /dev/null
+++ b/middleware/dnssec/black_lies_test.go
@@ -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")},
+ }
+}
diff --git a/middleware/dnssec/cache.go b/middleware/dnssec/cache.go
new file mode 100644
index 000000000..2153c84cb
--- /dev/null
+++ b/middleware/dnssec/cache.go
@@ -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)
+}
diff --git a/middleware/dnssec/cache_test.go b/middleware/dnssec/cache_test.go
new file mode 100644
index 000000000..0039586d5
--- /dev/null
+++ b/middleware/dnssec/cache_test.go
@@ -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")
+ }
+}
diff --git a/middleware/dnssec/dnskey.go b/middleware/dnssec/dnskey.go
new file mode 100644
index 000000000..9ae437c54
--- /dev/null
+++ b/middleware/dnssec/dnskey.go
@@ -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
+}
diff --git a/middleware/dnssec/dnssec.go b/middleware/dnssec/dnssec.go
new file mode 100644
index 000000000..b0a328bee
--- /dev/null
+++ b/middleware/dnssec/dnssec.go
@@ -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
+)
diff --git a/middleware/dnssec/dnssec_test.go b/middleware/dnssec/dnssec_test.go
new file mode 100644
index 000000000..49b0d5d3a
--- /dev/null
+++ b/middleware/dnssec/dnssec_test.go
@@ -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
+`
+)
diff --git a/middleware/dnssec/handler.go b/middleware/dnssec/handler.go
new file mode 100644
index 000000000..0c0f4a61d
--- /dev/null
+++ b/middleware/dnssec/handler.go
@@ -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)
+}
diff --git a/middleware/dnssec/handler_test.go b/middleware/dnssec/handler_test.go
new file mode 100644
index 000000000..9e6cedf8a
--- /dev/null
+++ b/middleware/dnssec/handler_test.go
@@ -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`
diff --git a/middleware/dnssec/responsewriter.go b/middleware/dnssec/responsewriter.go
new file mode 100644
index 000000000..2a7cbb972
--- /dev/null
+++ b/middleware/dnssec/responsewriter.go
@@ -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
+}
diff --git a/middleware/dnssec/rrsig.go b/middleware/dnssec/rrsig.go
new file mode 100644
index 000000000..17bc1195b
--- /dev/null
+++ b/middleware/dnssec/rrsig.go
@@ -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
diff --git a/middleware/etcd/etcd.go b/middleware/etcd/etcd.go
index 4eb55f7ed..38eac7ab3 100644
--- a/middleware/etcd/etcd.go
+++ b/middleware/etcd/etcd.go
@@ -8,8 +8,8 @@ import (
"github.com/miekg/coredns/middleware"
"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/singleflight"
etcdc "github.com/coreos/etcd/client"
"golang.org/x/net/context"
diff --git a/middleware/etcd/lookup.go b/middleware/etcd/lookup.go
index 88f362fc7..35d4b7226 100644
--- a/middleware/etcd/lookup.go
+++ b/middleware/etcd/lookup.go
@@ -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.
// only a tad bit fishy...
old := state.QName()
+
+ state.Clear()
state.Req.Question[0].Name = "ns.dns." + zone
services, err := e.records(state, false)
if err != nil {
return nil, nil, err
}
+ // ... and reset
state.Req.Question[0].Name = old
for _, serv := range services {
diff --git a/middleware/etcd/setup_test.go b/middleware/etcd/setup_test.go
index b28602122..a695d43ce 100644
--- a/middleware/etcd/setup_test.go
+++ b/middleware/etcd/setup_test.go
@@ -10,8 +10,8 @@ import (
"github.com/miekg/coredns/middleware"
"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/singleflight"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
diff --git a/middleware/file/closest.go b/middleware/file/closest.go
index 6af033e64..741327fde 100644
--- a/middleware/file/closest.go
+++ b/middleware/file/closest.go
@@ -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.
if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name &&
nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain {
diff --git a/middleware/file/file.go b/middleware/file/file.go
index 441e8b94d..a99a64d6f 100644
--- a/middleware/file/file.go
+++ b/middleware/file/file.go
@@ -1,7 +1,7 @@
package file
import (
- "fmt"
+ "errors"
"io"
"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}
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()
zone := middleware.Zones(f.Zones.Names).Matches(qname)
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]
if !ok {
diff --git a/middleware/file/reload_test.go b/middleware/file/reload_test.go
index 1769c701e..1ba9f4bcf 100644
--- a/middleware/file/reload_test.go
+++ b/middleware/file/reload_test.go
@@ -11,7 +11,7 @@ import (
)
func TestZoneReload(t *testing.T) {
- fileName, rm, err := test.Zone(t, ".", reloadZoneTest)
+ fileName, rm, err := test.TempFile(t, ".", reloadZoneTest)
if err != nil {
t.Fatalf("failed to create zone: %s", err)
}
diff --git a/middleware/etcd/singleflight/singleflight.go b/middleware/singleflight/singleflight.go
index ff2c2ee4f..ff2c2ee4f 100644
--- a/middleware/etcd/singleflight/singleflight.go
+++ b/middleware/singleflight/singleflight.go
diff --git a/middleware/state.go b/middleware/state.go
index ec2b618a6..34e6cdfc3 100644
--- a/middleware/state.go
+++ b/middleware/state.go
@@ -15,9 +15,13 @@ type State struct {
Req *dns.Msg
W dns.ResponseWriter
- // Cache size after first call to Size or Do
+ // Cache size after first call to Size or Do.
size int
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.
@@ -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.
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.
func (s *State) IP() 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
// 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.
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
}
+// Clear clears all caching from State s.
+func (s *State) Clear() {
+ s.name = ""
+}
+
const (
doTrue = 1
doFalse = 2
diff --git a/middleware/test/file.go b/middleware/test/file.go
new file mode 100644
index 000000000..b6068a32b
--- /dev/null
+++ b/middleware/test/file.go
@@ -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
+}
diff --git a/middleware/test/helpers.go b/middleware/test/helpers.go
index 3096f126b..a01d7a306 100644
--- a/middleware/test/helpers.go
+++ b/middleware/test/helpers.go
@@ -45,17 +45,18 @@ func (c Case) Msg() *dns.Msg {
return m
}
-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 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 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 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 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 NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) }
+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 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 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 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 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 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 {
o := new(dns.OPT)
diff --git a/middleware/test/zone.go b/middleware/test/zone.go
deleted file mode 100644
index 490280a7a..000000000
--- a/middleware/test/zone.go
+++ /dev/null
@@ -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
-}
diff --git a/test/proxy_test.go b/test/proxy_test.go
index 9365570b6..74e60148a 100644
--- a/test/proxy_test.go
+++ b/test/proxy_test.go
@@ -21,7 +21,7 @@ example.org. IN A 127.0.0.1
`
func TestLookupProxy(t *testing.T) {
- name, rm, err := test.Zone(t, ".", exampleOrg)
+ name, rm, err := test.TempFile(t, ".", exampleOrg)
if err != nil {
t.Fatalf("failed to created zone: %s", err)
}