Remove the word middleware (#1067)

* Rename middleware to plugin

first pass; mostly used 'sed', few spots where I manually changed
text.

This still builds a coredns binary.

* fmt error

* Rename AddMiddleware to AddPlugin

* Readd AddMiddleware to remain backwards compat
This commit is contained in:
Miek Gieben
2017-09-14 09:36:06 +01:00
committed by GitHub
parent b984aa4559
commit d8714e64e4
354 changed files with 974 additions and 969 deletions

88
plugin/dnssec/README.md Normal file
View File

@@ -0,0 +1,88 @@
# dnssec
*dnssec* enables on-the-fly DNSSEC signing of served data.
## Syntax
~~~
dnssec [ZONES... ] {
key file KEY...
cache_capacity CAPACITY
}
~~~
The specified key is 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). NSEC3 is *not* supported.
If multiple *dnssec* plugins are specified in the same zone, the last one specified will be
used ( see [bugs](#bugs) ).
* `ZONES` zones that should be signed. If empty, the zones from the configuration block
are used.
* `key file` indicates that key file(s) should be read from disk. When multiple keys are specified, RRsets
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*. The name of the
key file can be specified as one of the following formats
* basename of the generated key `Kexample.org+013+45330`
* generated public key `Kexample.org+013+45330.key`
* generated private key `Kexample.org+013+45330.private`
* `cache_capacity` indicates the capacity of the cache. The dnssec plugin uses a cache to store
RRSIGs. The default capacity is 10000.
## Metrics
If monitoring is enabled (via the *prometheus* directive) then the following metrics are exported:
* coredns_dnssec_cache_size{type} - total elements in the cache, type is "signature".
* coredns_dnssec_cache_capacity{type} - total capacity of the cache, type is "signature".
* coredns_dnssec_cache_hits_total - Counter of cache hits.
* coredns_dnssec_cache_misses_total - Counter of cache misses.
## Examples
Sign responses for `example.org` with the key "Kexample.org.+013+45330.key".
~~~
example.org:53 {
dnssec {
key file /etc/coredns/Kexample.org.+013+45330
}
whoami
}
~~~
Sign responses for a kubernetes zone with the key "Kcluster.local+013+45129.key".
~~~
cluster.local:53 {
kubernetes cluster.local
dnssec cluster.local {
key file /etc/coredns/Kcluster.local+013+45129
}
}
~~~
## Bugs
Multiple *dnssec* plugins inside one server stanza will silently overwrite earlier ones, here
`example.local` will overwrite the one for `cluster.local`.
~~~
.:53 {
kubernetes cluster.local
dnssec cluster.local {
key file /etc/coredns/cluster.local
}
dnssec example.local {
key file /etc/coredns/example.local
}
whoami
}
~~~

View File

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

View File

@@ -0,0 +1,49 @@
package dnssec
import (
"testing"
"time"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestZoneSigningBlackLies(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testNxdomainMsg()
state := request.Request{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)
}
}
func testNxdomainMsg() *dns.Msg {
return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError},
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")},
}
}

22
plugin/dnssec/cache.go Normal file
View File

@@ -0,0 +1,22 @@
package dnssec
import (
"hash/fnv"
"github.com/miekg/dns"
)
// hash serializes the RRset and return a signature cache key.
func hash(rrs []dns.RR) uint32 {
h := fnv.New32()
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.Sum32()
return i
}

View File

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

72
plugin/dnssec/dnskey.go Normal file
View File

@@ -0,0 +1,72 @@
package dnssec
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"errors"
"os"
"time"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// DNSKEY holds a DNSSEC public and private key used for on-the-fly signing.
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 request.Request, 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
}

135
plugin/dnssec/dnssec.go Normal file
View File

@@ -0,0 +1,135 @@
// Package dnssec implements a plugin that signs responses on-the-fly using
// NSEC black lies.
package dnssec
import (
"time"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/cache"
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/coredns/coredns/plugin/pkg/singleflight"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// Dnssec signs the reply on-the-fly.
type Dnssec struct {
Next plugin.Handler
zones []string
keys []*DNSKEY
inflight *singleflight.Group
cache *cache.Cache
}
// New returns a new Dnssec.
func New(zones []string, keys []*DNSKEY, next plugin.Handler, c *cache.Cache) Dnssec {
return Dnssec{Next: next,
zones: zones,
keys: keys,
cache: c,
inflight: new(singleflight.Group),
}
}
// Sign signs the message in state. 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 request.Request, zone string, now time.Time) *dns.Msg {
req := state.Req
mt, _ := response.Typify(req, time.Now().UTC()) // TODO(miek): need opt record here?
if mt == response.Delegation {
// TODO(miek): uh, signing DS record?!?!
return req
}
incep, expir := incepExpir(now)
if mt == response.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(sigs, req.Extra...) // prepend to leave OPT alone
}
}
return req
}
func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) {
k := hash(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 uint32, sigs []dns.RR) {
d.cache.Add(key, sigs)
}
func (d Dnssec) get(key uint32) ([]dns.RR, bool) {
if s, ok := d.cache.Get(key); ok {
cacheHits.Inc()
return s.([]dns.RR), true
}
cacheMisses.Inc()
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 (
eightDays = 8 * 24 * time.Hour
defaultCap = 10000 // default capacity of the cache.
)

View File

@@ -0,0 +1,219 @@
package dnssec
import (
"testing"
"time"
"github.com/coredns/coredns/plugin/pkg/cache"
"github.com/coredns/coredns/plugin/test"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestZoneSigning(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsg()
state := request.Request{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(".", privKey1)
fPub1, rmPub1, _ := test.TempFile(".", 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 := request.Request{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")
}
}
// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org.
func TestSigningDifferentZone(t *testing.T) {
fPriv, rmPriv, _ := test.TempFile(".", privKey)
fPub, rmPub, _ := test.TempFile(".", 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 := request.Request{Req: m}
c := cache.New(defaultCap)
d := New([]string{"example.org."}, []*DNSKEY{key}, nil, c)
m = d.Sign(state, "example.org.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
t.Logf("%+v\n", m)
}
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 := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("answer section should have 1 sig")
}
}
func TestZoneSigningDelegation(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testDelegationMsg()
state := request.Request{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 TestSigningDname(t *testing.T) {
d, rm1, rm2 := newDnssec(t, []string{"miek.nl."})
defer rm1()
defer rm2()
m := testMsgDname()
state := request.Request{Req: m}
// We sign *everything* we see, also the synthesized CNAME.
m = d.Sign(state, "miek.nl.", time.Now().UTC())
if !section(m.Answer, 3) {
t.Errorf("answer section should have 3 sig")
}
}
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 testMsgDname() *dns.Msg {
return &dns.Msg{
Answer: []dns.RR{
test.CNAME("a.dname.miek.nl. 1800 IN CNAME a.test.miek.nl."),
test.A("a.test.miek.nl. 1800 IN A 139.162.196.78"),
test.DNAME("dname.miek.nl. 1800 IN DNAME test.miek.nl."),
},
}
}
func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) {
k, rm1, rm2 := newKey(t)
c := cache.New(defaultCap)
d := New(zones, []*DNSKEY{k}, nil, c)
return d, rm1, rm2
}
func newKey(t *testing.T) (*DNSKEY, func(), func()) {
fPriv, rmPriv, _ := test.TempFile(".", privKey)
fPub, rmPub, _ := test.TempFile(".", 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
`
)

82
plugin/dnssec/handler.go Normal file
View File

@@ -0,0 +1,82 @@
package dnssec
import (
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
// ServeDNS implements the plugin.Handler interface.
func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
do := state.Do()
qname := state.Name()
qtype := state.QType()
zone := plugin.Zones(d.zones).Matches(qname)
if zone == "" {
return plugin.NextOrFailure(d.Name(), d.Next, 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)
resp.Authoritative = true
state.SizeAndDo(resp)
w.WriteMsg(resp)
return dns.RcodeSuccess, nil
}
}
}
drr := &ResponseWriter{w, d}
return plugin.NextOrFailure(d.Name(), d.Next, ctx, drr, r)
}
var (
cacheSize = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "cache_size",
Help: "The number of elements in the dnssec cache.",
}, []string{"type"})
cacheCapacity = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "cache_capacity",
Help: "The dnssec cache's capacity.",
}, []string{"type"})
cacheHits = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "cache_hits_total",
Help: "The count of cache hits.",
})
cacheMisses = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: subsystem,
Name: "cache_misses_total",
Help: "The count of cache misses.",
})
)
// Name implements the Handler interface.
func (d Dnssec) Name() string { return "dnssec" }
const subsystem = "dnssec"
func init() {
prometheus.MustRegister(cacheSize)
prometheus.MustRegister(cacheCapacity)
prometheus.MustRegister(cacheHits)
prometheus.MustRegister(cacheMisses)
}

View File

@@ -0,0 +1,155 @@
package dnssec
import (
"strings"
"testing"
"github.com/coredns/coredns/plugin/file"
"github.com/coredns/coredns/plugin/pkg/cache"
"github.com/coredns/coredns/plugin/pkg/dnsrecorder"
"github.com/coredns/coredns/plugin/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."),
},
Ns: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
},
},
{
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"),
},
Ns: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="),
},
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"),
},
Ns: []dns.RR{
test.NS("miek.nl. 1800 IN NS linode.atoom.net."),
test.RRSIG("miek.nl. 1800 IN RRSIG NS 13 2 3600 20161217114912 20161209084912 18512 miek.nl. ad9gA8VWgF1H8ze9/0Rk2Q=="),
},
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", 0)
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()
c := cache.New(defaultCap)
dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm, c)
ctx := context.TODO()
for _, tc := range dnsTestCases {
m := tc.Msg()
rec := dnsrecorder.New(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg
test.SortAndCheck(t, resp, tc)
}
}
func TestLookupDNSKEY(t *testing.T) {
dnskey, rm1, rm2 := newKey(t)
defer rm1()
defer rm2()
c := cache.New(defaultCap)
dh := New([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler(), c)
ctx := context.TODO()
for _, tc := range dnssecTestCases {
m := tc.Msg()
rec := dnsrecorder.New(&test.ResponseWriter{})
_, err := dh.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg
if !resp.Authoritative {
t.Errorf("Authoritative Answer should be true, got false")
}
test.SortAndCheck(t, resp, tc)
}
}
const dbMiekNL = `
$TTL 30M
$ORIGIN miek.nl.
@ IN SOA linode.atoom.net. miek.miek.nl. (
1282630057 ; Serial
4H ; Refresh
1H ; Retry
7D ; Expire
4H ) ; Negative Cache TTL
IN NS linode.atoom.net.
IN MX 1 aspmx.l.google.com.
IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
a IN A 139.162.196.78
IN AAAA 2a01:7e00::f03c:91ff:fef1:6735
www IN CNAME a`

View File

@@ -0,0 +1,49 @@
package dnssec
import (
"log"
"time"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
// ResponseWriter sign the response on the fly.
type ResponseWriter struct {
dns.ResponseWriter
d Dnssec
}
// WriteMsg implements the dns.ResponseWriter interface.
func (d *ResponseWriter) 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 := request.Request{W: d.ResponseWriter, Req: res}
qname := state.Name()
zone := plugin.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())
cacheSize.WithLabelValues("signature").Set(float64(d.d.cache.Len()))
}
state.SizeAndDo(res)
return d.ResponseWriter.WriteMsg(res)
}
// Write implements the dns.ResponseWriter interface.
func (d *ResponseWriter) Write(buf []byte) (int, error) {
log.Printf("[WARNING] Dnssec called with Write: not signing reply")
n, err := d.ResponseWriter.Write(buf)
return n, err
}
// Hijack implements the dns.ResponseWriter interface.
func (d *ResponseWriter) Hijack() { d.ResponseWriter.Hijack() }

53
plugin/dnssec/rrsig.go Normal file
View File

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

128
plugin/dnssec/setup.go Normal file
View File

@@ -0,0 +1,128 @@
package dnssec
import (
"fmt"
"strconv"
"strings"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/cache"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("dnssec", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
zones, keys, capacity, err := dnssecParse(c)
if err != nil {
return plugin.Error("dnssec", err)
}
ca := cache.New(capacity)
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return New(zones, keys, next, ca)
})
// Export the capacity for the metrics. This only happens once, because this is a re-load change only.
cacheCapacity.WithLabelValues("signature").Set(float64(capacity))
return nil
}
func dnssecParse(c *caddy.Controller) ([]string, []*DNSKEY, int, error) {
zones := []string{}
keys := []*DNSKEY{}
capacity := defaultCap
for c.Next() {
// dnssec [zones...]
zones = make([]string, len(c.ServerBlockKeys))
copy(zones, c.ServerBlockKeys)
args := c.RemainingArgs()
if len(args) > 0 {
zones = args
}
for c.NextBlock() {
switch c.Val() {
case "key":
k, e := keyParse(c)
if e != nil {
return nil, nil, 0, e
}
keys = append(keys, k...)
case "cache_capacity":
if !c.NextArg() {
return nil, nil, 0, c.ArgErr()
}
value := c.Val()
cacheCap, err := strconv.Atoi(value)
if err != nil {
return nil, nil, 0, err
}
capacity = cacheCap
}
}
}
for i := range zones {
zones[i] = plugin.Host(zones[i]).Normalize()
}
// Check if each keys owner name can actually sign the zones we want them to sign
for _, k := range keys {
kname := plugin.Name(k.K.Header().Name)
ok := false
for i := range zones {
if kname.Matches(zones[i]) {
ok = true
break
}
}
if !ok {
return zones, keys, capacity, fmt.Errorf("key %s (keyid: %d) can not sign any of the zones", string(kname), k.keytag)
}
}
return zones, keys, capacity, nil
}
func keyParse(c *caddy.Controller) ([]*DNSKEY, error) {
keys := []*DNSKEY{}
if !c.NextArg() {
return nil, c.ArgErr()
}
value := c.Val()
if value == "file" {
ks := c.RemainingArgs()
if len(ks) == 0 {
return nil, c.ArgErr()
}
for _, k := range ks {
base := k
// Kmiek.nl.+013+26205.key, handle .private or without extension: Kmiek.nl.+013+26205
if strings.HasSuffix(k, ".key") {
base = k[:len(k)-4]
}
if strings.HasSuffix(k, ".private") {
base = k[:len(k)-8]
}
k, err := ParseKeyFile(base+".key", base+".private")
if err != nil {
return nil, err
}
keys = append(keys, k)
}
}
return keys, nil
}

120
plugin/dnssec/setup_test.go Normal file
View File

@@ -0,0 +1,120 @@
package dnssec
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/mholt/caddy"
)
func TestSetupDnssec(t *testing.T) {
if err := ioutil.WriteFile("Kcluster.local.key", []byte(keypub), 0644); err != nil {
t.Fatalf("Failed to write pub key file: %s", err)
}
defer func() { os.Remove("Kcluster.local.key") }()
if err := ioutil.WriteFile("Kcluster.local.private", []byte(keypriv), 0644); err != nil {
t.Fatalf("Failed to write private key file: %s", err)
}
defer func() { os.Remove("Kcluster.local.private") }()
tests := []struct {
input string
shouldErr bool
expectedZones []string
expectedKeys []string
expectedCapacity int
expectedErrContent string
}{
{`dnssec`, false, nil, nil, defaultCap, ""},
{`dnssec example.org`, false, []string{"example.org."}, nil, defaultCap, ""},
{`dnssec 10.0.0.0/8`, false, []string{"10.in-addr.arpa."}, nil, defaultCap, ""},
{
`dnssec example.org {
cache_capacity 100
}`, false, []string{"example.org."}, nil, 100, "",
},
{
`dnssec cluster.local {
key file Kcluster.local
}`, false, []string{"cluster.local."}, nil, defaultCap, "",
},
{
`dnssec example.org cluster.local {
key file Kcluster.local
}`, false, []string{"example.org.", "cluster.local."}, nil, defaultCap, "",
},
// fails
{
`dnssec example.org {
key file Kcluster.local
}`, true, []string{"example.org."}, nil, defaultCap, "can not sign any",
},
{
`dnssec example.org {
key
}`, true, []string{"example.org."}, nil, defaultCap, "argument count",
},
{
`dnssec example.org {
key file
}`, true, []string{"example.org."}, nil, defaultCap, "argument count",
},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
zones, keys, capacity, 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)
}
}
if capacity != test.expectedCapacity {
t.Errorf("Dnssec not correctly set capacity for input '%s' Expected: '%d', actual: '%d'", test.input, capacity, test.expectedCapacity)
}
}
}
}
const keypub = `; This is a zone-signing key, keyid 45330, for cluster.local.
; Created: 20170901060531 (Fri Sep 1 08:05:31 2017)
; Publish: 20170901060531 (Fri Sep 1 08:05:31 2017)
; Activate: 20170901060531 (Fri Sep 1 08:05:31 2017)
cluster.local. IN DNSKEY 256 3 5 AwEAAcFpDv+Cb23kFJowu+VU++b2N1uEHi6Ll9H0BzLasFOdJjEEclCO q/KlD4682vOMXxJNN8ZwOyiCa7Y0TEYqSwWvhHyn3bHCwuy4I6fss4Wd 7Y9dU+6QTgJ8LimGG40Iizjc9zqoU8Q+q81vIukpYWOHioHoY7hsWBvS RSlzDJk3`
const keypriv = `Private-key-format: v1.3
Algorithm: 5 (RSASHA1)
Modulus: wWkO/4JvbeQUmjC75VT75vY3W4QeLouX0fQHMtqwU50mMQRyUI6r8qUPjrza84xfEk03xnA7KIJrtjRMRipLBa+EfKfdscLC7Lgjp+yzhZ3tj11T7pBOAnwuKYYbjQiLONz3OqhTxD6rzW8i6SlhY4eKgehjuGxYG9JFKXMMmTc=
PublicExponent: AQAB
PrivateExponent: K5XyZFBPrjMVFX5gCZlyPyVDamNGrfSVXSIiMSqpS96BSdCXtmHAjCj4bZFPwkzi6+vs4tJN8p4ZifEVM0a6qwPZyENBrc2qbsweOXE6l8BaPVWFX30xvVRzGXuNtXxlBXE17zoHty5r5mRyRou1bc2HUS5otdkEjE30RiocQVk=
Prime1: 7RRFUxaZkVNVH1DaT/SV5Sb8kABB389qLwU++argeDCVf+Wm9BBlTrsz2U6bKlfpaUmYZKtCCd+CVxqzMyuu0w==
Prime2: 0NiY3d7Fa08IGY9L4TaFc02A721YcDNBBf95BP31qGvwnYsLFM/1xZwaEsIjohg8g+m/GpyIlvNMbK6pywIVjQ==
Exponent1: XjXO8pype9mMmvwrNNix9DTQ6nxfsQugW30PMHGZ78kGr6NX++bEC0xS50jYWjRDGcbYGzD+9iNujSScD3qNZw==
Exponent2: wkoOhLIfhUIj7etikyUup2Ld5WAbW15DSrotstg0NrgcQ+Q7reP96BXeJ79WeREFE09cyvv/EjdLzPv81/CbbQ==
Coefficient: ah4LL0KLTO8kSKHK+X9Ud8grYi94QSNdbX11ge/eFcS/41QhDuZRTAFv4y0+IG+VWd+XzojLsQs+jzLe5GzINg==
Created: 20170901060531
Publish: 20170901060531
Activate: 20170901060531
`