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:
parent
8e6c690484
commit
1aa1a92198
39 changed files with 1206 additions and 144 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
||||||
query.log
|
query.log
|
||||||
Corefile
|
Corefile
|
||||||
|
*.swp
|
||||||
|
coredns
|
||||||
|
|
|
@ -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},
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
79
core/setup/dnssec.go
Normal 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
54
core/setup/dnssec_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
92
middleware/cache/cache.go
vendored
92
middleware/cache/cache.go
vendored
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
6
middleware/cache/cache_test.go
vendored
6
middleware/cache/cache_test.go
vendored
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
5
middleware/cache/handler.go
vendored
5
middleware/cache/handler.go
vendored
|
@ -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
52
middleware/classify.go
Normal 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
|
||||||
|
}
|
31
middleware/classify_test.go
Normal file
31
middleware/classify_test.go
Normal 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"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
35
middleware/dnssec/README.md
Normal file
35
middleware/dnssec/README.md
Normal 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
|
24
middleware/dnssec/black_lies.go
Normal file
24
middleware/dnssec/black_lies.go
Normal 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
|
||||||
|
}
|
50
middleware/dnssec/black_lies_test.go
Normal file
50
middleware/dnssec/black_lies_test.go
Normal 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")},
|
||||||
|
}
|
||||||
|
}
|
23
middleware/dnssec/cache.go
Normal file
23
middleware/dnssec/cache.go
Normal 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)
|
||||||
|
}
|
32
middleware/dnssec/cache_test.go
Normal file
32
middleware/dnssec/cache_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
71
middleware/dnssec/dnskey.go
Normal file
71
middleware/dnssec/dnskey.go
Normal 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
127
middleware/dnssec/dnssec.go
Normal 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
|
||||||
|
)
|
193
middleware/dnssec/dnssec_test.go
Normal file
193
middleware/dnssec/dnssec_test.go
Normal 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
|
||||||
|
`
|
||||||
|
)
|
61
middleware/dnssec/handler.go
Normal file
61
middleware/dnssec/handler.go
Normal 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)
|
||||||
|
}
|
170
middleware/dnssec/handler_test.go
Normal file
170
middleware/dnssec/handler_test.go
Normal 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`
|
48
middleware/dnssec/responsewriter.go
Normal file
48
middleware/dnssec/responsewriter.go
Normal 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
|
||||||
|
}
|
53
middleware/dnssec/rrsig.go
Normal file
53
middleware/dnssec/rrsig.go
Normal 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
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
20
middleware/test/file.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue