plugin/dnssec: implement shotgun from CloudFlare (#1305)

* plugin/dnssec: implement shotgun from CloudFlare

Put a whole bunch of types in the NSEC bitmap and remove the one that's
being asked for.

Add more records for queries to the apex, SOA, DNSKEY, MX.
This commit is contained in:
Miek Gieben 2018-01-03 11:11:56 +00:00 committed by GitHub
parent 7fe5b0bb1f
commit 311af9314d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 33 deletions

View file

@ -1,24 +1,65 @@
package dnssec
import "github.com/miekg/dns"
import (
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/coredns/coredns/request"
"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 )
// 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) {
func (d Dnssec) nsec(state request.Request, mt response.Type, 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}
nsec.Hdr = dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC}
nsec.NextDomain = "\\000." + state.QName()
if state.Name() == state.Zone {
nsec.TypeBitMap = filter18(state.QType(), apexBitmap, mt)
} else {
nsec.TypeBitMap = filter14(state.QType(), zoneBitmap, mt)
}
sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir)
sigs, err := d.sign([]dns.RR{nsec}, state.Zone, ttl, incep, expir)
if err != nil {
return nil, err
}
return append(sigs, nsec), nil
}
// The NSEC bit maps we return.
var (
zoneBitmap = [...]uint16{dns.TypeA, dns.TypeHINFO, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
apexBitmap = [...]uint16{dns.TypeA, dns.TypeNS, dns.TypeSOA, dns.TypeHINFO, dns.TypeMX, dns.TypeTXT, dns.TypeAAAA, dns.TypeLOC, dns.TypeSRV, dns.TypeCERT, dns.TypeSSHFP, dns.TypeRRSIG, dns.TypeNSEC, dns.TypeDNSKEY, dns.TypeTLSA, dns.TypeHIP, dns.TypeOPENPGPKEY, dns.TypeSPF}
)
// filter14 filters out t from bitmap (if it exists). If mt is not an NODATA response, just
// return the entire bitmap.
func filter14(t uint16, bitmap [14]uint16, mt response.Type) []uint16 {
if mt != response.NoData {
return zoneBitmap[:]
}
for i := range bitmap {
if bitmap[i] == t {
return append(bitmap[:i], bitmap[i+1:]...)
}
}
return zoneBitmap[:] // make a slice
}
func filter18(t uint16, bitmap [18]uint16, mt response.Type) []uint16 {
if mt != response.NoData {
return apexBitmap[:]
}
for i := range bitmap {
if bitmap[i] == t {
return append(bitmap[:i], bitmap[i+1:]...)
}
}
return apexBitmap[:] // make a slice
}

View file

@ -16,8 +16,8 @@ func TestZoneSigningBlackLies(t *testing.T) {
defer rm2()
m := testNxdomainMsg()
state := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC())
if !section(m.Ns, 2) {
t.Errorf("authority section should have 2 sig")
}

View file

@ -22,10 +22,10 @@ func TestCacheSet(t *testing.T) {
c := cache.New(defaultCap)
m := testMsg()
state := request.Request{Req: m}
state := request.Request{Req: m, Zone: "miek.nl."}
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())
d.Sign(state, time.Now().UTC())
_, ok := d.get(k)
if !ok {

View file

@ -39,7 +39,7 @@ func New(zones []string, keys []*DNSKEY, next plugin.Handler, c *cache.Cache) Dn
// will insert DS records and sign those.
// Signatures 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 {
func (d Dnssec) Sign(state request.Request, now time.Time) *dns.Msg {
req := state.Req
incep, expir := incepExpir(now)
@ -71,10 +71,10 @@ func (d Dnssec) Sign(state request.Request, zone string, now time.Time) *dns.Msg
ttl := req.Ns[0].Header().Ttl
if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil {
if sigs, err := d.sign(req.Ns, state.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 {
if sigs, err := d.nsec(state, mt, ttl, incep, expir); err == nil {
req.Ns = append(req.Ns, sigs...)
}
if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode
@ -85,19 +85,19 @@ func (d Dnssec) Sign(state request.Request, zone string, now time.Time) *dns.Msg
for _, r := range rrSets(req.Answer) {
ttl := r[0].Header().Ttl
if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil {
if sigs, err := d.sign(r, state.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 {
if sigs, err := d.sign(r, state.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 {
if sigs, err := d.sign(r, state.Zone, ttl, incep, expir); err == nil {
req.Extra = append(sigs, req.Extra...) // prepend to leave OPT alone
}
}

View file

@ -17,9 +17,9 @@ func TestZoneSigning(t *testing.T) {
defer rm2()
m := testMsg()
state := request.Request{Req: m}
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
m = d.Sign(state, time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("Answer section should have 1 RRSIG")
}
@ -45,8 +45,8 @@ func TestZoneSigningDouble(t *testing.T) {
d.keys = append(d.keys, key1)
m := testMsg()
state := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC())
if !section(m.Answer, 2) {
t.Errorf("Answer section should have 1 RRSIG")
}
@ -68,10 +68,10 @@ func TestSigningDifferentZone(t *testing.T) {
}
m := testMsgEx()
state := request.Request{Req: m}
state := request.Request{Req: m, Zone: "example.org."}
c := cache.New(defaultCap)
d := New([]string{"example.org."}, []*DNSKEY{key}, nil, c)
m = d.Sign(state, "example.org.", time.Now().UTC())
m = d.Sign(state, time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("Answer section should have 1 RRSIG")
t.Logf("%+v\n", m)
@ -88,8 +88,8 @@ func TestSigningCname(t *testing.T) {
defer rm2()
m := testMsgCname()
state := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC())
if !section(m.Answer, 1) {
t.Errorf("Answer section should have 1 RRSIG")
}
@ -102,8 +102,8 @@ func testZoneSigningDelegation(t *testing.T) {
defer rm2()
m := testDelegationMsg()
state := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC())
if !section(m.Ns, 1) {
t.Errorf("Authority section should have 1 RRSIG")
t.Logf("%v\n", m)
@ -132,9 +132,9 @@ func TestSigningDname(t *testing.T) {
defer rm2()
m := testMsgDname()
state := request.Request{Req: m}
state := request.Request{Req: m, Zone: "miek.nl."}
// We sign *everything* we see, also the synthesized CNAME.
m = d.Sign(state, "miek.nl.", time.Now().UTC())
m = d.Sign(state, time.Now().UTC())
if !section(m.Answer, 3) {
t.Errorf("Answer section should have 3 RRSIGs")
}
@ -147,8 +147,8 @@ func TestSigningEmpty(t *testing.T) {
m := testEmptyMsg()
m.SetQuestion("a.miek.nl.", dns.TypeA)
state := request.Request{Req: m}
m = d.Sign(state, "miek.nl.", time.Now().UTC())
state := request.Request{Req: m, Zone: "miek.nl."}
m = d.Sign(state, time.Now().UTC())
if !section(m.Ns, 2) {
t.Errorf("Authority section should have 2 RRSIGs")
}

View file

@ -23,6 +23,8 @@ func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
}
state.Zone = zone
// Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let
// the query through.
if qtype == dns.TypeDNSKEY {

View file

@ -72,10 +72,30 @@ var dnsTestCases = []test.Case{
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "wwwww.miek.nl.", Qtype: dns.TypeAAAA, Do: true,
Ns: []dns.RR{
test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220135446 20171212105446 18512 miek.nl. hCRzzjYz6w=="),
test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
test.NSEC("wwwww.miek.nl. 1800 IN NSEC \\000.wwwww.miek.nl. A HINFO TXT LOC SRV CERT SSHFP RRSIG NSEC TLSA HIP OPENPGPKEY SPF"),
test.RRSIG("wwwww.miek.nl. 1800 IN RRSIG NSEC 13 3 3600 20171220135446 20171212105446 18512 miek.nl. cVUQWs8xw=="),
},
Extra: []dns.RR{test.OPT(4096, true)},
},
{
Qname: "miek.nl.", Qtype: dns.TypeHINFO, Do: true,
Ns: []dns.RR{
test.NSEC("miek.nl. 1800 IN NSEC \\000.miek.nl. A NS SOA MX TXT AAAA LOC SRV CERT SSHFP RRSIG NSEC DNSKEY TLSA HIP OPENPGPKEY SPF"),
test.RRSIG("miek.nl. 1800 IN RRSIG NSEC 13 2 3600 20171220141741 20171212111741 18512 miek.nl. GuXROL7Uu+UiPcg=="),
test.RRSIG("miek.nl. 1800 IN RRSIG SOA 13 2 3600 20171220141741 20171212111741 18512 miek.nl. 8bLTReqmuQtw=="),
test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
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.
// Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS on ServerFailure
},
}
@ -131,6 +151,17 @@ func TestLookupDNSKEY(t *testing.T) {
}
test.SortAndCheck(t, resp, tc)
// If there is an NSEC present in authority section check if the bitmap does not have the qtype set.
for _, rr := range resp.Ns {
if n, ok := rr.(*dns.NSEC); ok {
for i := range n.TypeBitMap {
if n.TypeBitMap[i] == tc.Qtype {
t.Errorf("bitmap contains qtype: %d", tc.Qtype)
}
}
}
}
}
}

View file

@ -26,9 +26,10 @@ func (d *ResponseWriter) WriteMsg(res *dns.Msg) error {
if zone == "" {
return d.ResponseWriter.WriteMsg(res)
}
state.Zone = zone
if state.Do() {
res = d.d.Sign(state, zone, time.Now().UTC())
res = d.d.Sign(state, time.Now().UTC())
cacheSize.WithLabelValues("signature").Set(float64(d.d.cache.Len()))
}