mirror of
https://github.com/coredns/coredns.git
synced 2026-01-14 04:41:18 -05:00
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:
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
|
||||
Reference in New Issue
Block a user