diff options
author | 2016-04-26 17:57:11 +0100 | |
---|---|---|
committer | 2016-04-26 17:57:11 +0100 | |
commit | 1aa1a9219825e9f203eff718cb85360df706e542 (patch) | |
tree | 5a046639aeae5e1c2e3b6a62c5f19f7231de3cc6 /middleware | |
parent | 8e6c690484e3bab2dbebcf1958bfef168be6b922 (diff) | |
download | coredns-1aa1a9219825e9f203eff718cb85360df706e542.tar.gz coredns-1aa1a9219825e9f203eff718cb85360df706e542.tar.zst coredns-1aa1a9219825e9f203eff718cb85360df706e542.zip |
Add middleware/dnssec (#133)
This adds an online dnssec middleware. The middleware will sign
responses on the fly. Negative responses are signed with NSEC black
lies.
Diffstat (limited to 'middleware')
28 files changed, 1057 insertions, 127 deletions
diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 1ec71b047..71df68fdd 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -1,33 +1,5 @@ package cache -/* -The idea behind this implementation is as follows. We have a cache that is index -by a couple different keys, which allows use to have: - -- negative cache: qname only for NXDOMAIN responses -- negative cache: qname + qtype for NODATA responses -- positive cache: qname + qtype for succesful responses. - -We track DNSSEC responses separately, i.e. under a different cache key. -Each Item stored contains the message split up in the different sections -and a few bits of the msg header. - -For instance an NXDOMAIN for blaat.miek.nl will create the -following negative cache entry (do signal state of DO (do off, DO on)). - - ncache: do <blaat.miek.nl> - Item: - Ns: <miek.nl> SOA RR - -If found a return packet is assembled and returned to the client. Taking size and EDNS0 -constraints into account. - -We also need to track if the answer received was an authoritative answer, ad bit and other -setting, for this we also store a few header bits. - -For the positive cache we use the same idea. Truncated responses are never stored. -*/ - import ( "log" "time" @@ -50,41 +22,7 @@ func NewCache(ttl int, zones []string, next middleware.Handler) Cache { return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second} } -type messageType int - -const ( - success messageType = iota - nameError // NXDOMAIN in header, SOA in auth. - noData // NOERROR in header, SOA in auth. - otherError // Don't cache these. -) - -// classify classifies a message, it returns the MessageType. -func classify(m *dns.Msg) (messageType, *dns.OPT) { - opt := m.IsEdns0() - soa := false - if m.Rcode == dns.RcodeSuccess { - return success, opt - } - for _, r := range m.Ns { - if r.Header().Rrtype == dns.TypeSOA { - soa = true - break - } - } - - // Check length of different section, and drop stuff that is just to large. - if soa && m.Rcode == dns.RcodeSuccess { - return noData, opt - } - if soa && m.Rcode == dns.RcodeNameError { - return nameError, opt - } - - return otherError, opt -} - -func cacheKey(m *dns.Msg, t messageType, do bool) string { +func cacheKey(m *dns.Msg, t middleware.MsgType, do bool) string { if m.Truncated { return "" } @@ -92,13 +30,15 @@ func cacheKey(m *dns.Msg, t messageType, do bool) string { qtype := m.Question[0].Qtype qname := middleware.Name(m.Question[0].Name).Normalize() switch t { - case success: + case middleware.Success: + fallthrough + case middleware.Delegation: return successKey(qname, qtype, do) - case nameError: + case middleware.NameError: return nameErrorKey(qname, do) - case noData: + case middleware.NoData: return noDataKey(qname, qtype, do) - case otherError: + case middleware.OtherError: return "" } return "" @@ -116,13 +56,13 @@ func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap tim func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { do := false - mt, opt := classify(res) + mt, opt := middleware.Classify(res) if opt != nil { do = opt.Do() } key := cacheKey(res, mt, do) - c.Set(res, key, mt) + c.set(res, key, mt) if c.cap != 0 { setCap(res, uint32(c.cap.Seconds())) @@ -131,7 +71,7 @@ func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error { return c.ResponseWriter.WriteMsg(res) } -func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { +func (c *CachingResponseWriter) set(m *dns.Msg, key string, mt middleware.MsgType) { if key == "" { // logger the log? TODO(miek) return @@ -139,14 +79,14 @@ func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) { duration := c.cap switch mt { - case success: + case middleware.Success, middleware.Delegation: if c.cap == 0 { duration = minTtl(m.Answer, mt) } i := newItem(m, duration) c.cache.Set(key, i, duration) - case nameError, noData: + case middleware.NameError, middleware.NoData: if c.cap == 0 { duration = minTtl(m.Ns, mt) } @@ -167,19 +107,19 @@ func (c *CachingResponseWriter) Hijack() { return } -func minTtl(rrs []dns.RR, mt messageType) time.Duration { - if mt != success && mt != nameError && mt != noData { +func minTtl(rrs []dns.RR, mt middleware.MsgType) time.Duration { + if mt != middleware.Success && mt != middleware.NameError && mt != middleware.NoData { return 0 } minTtl := maxTtl for _, r := range rrs { switch mt { - case nameError, noData: + case middleware.NameError, middleware.NoData: if r.Header().Rrtype == dns.TypeSOA { return time.Duration(r.(*dns.SOA).Minttl) * time.Second } - case success: + case middleware.Success, middleware.Delegation: if r.Header().Ttl < minTtl { minTtl = r.Header().Ttl } diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 310a1164e..452831082 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -78,13 +78,13 @@ func TestCache(t *testing.T) { m = cacheMsg(m, tc) do := tc.in.Do - mt, _ := classify(m) + mt, _ := middleware.Classify(m) key := cacheKey(m, mt, do) - crr.Set(m, key, mt) + crr.set(m, key, mt) name := middleware.Name(m.Question[0].Name).Normalize() qtype := m.Question[0].Qtype - i, ok := c.Get(name, qtype, do) + i, ok := c.get(name, qtype, do) if !ok && !m.Truncated { t.Errorf("Truncated message should not have been cached") } diff --git a/middleware/cache/handler.go b/middleware/cache/handler.go index 35443fa1d..b891d7278 100644 --- a/middleware/cache/handler.go +++ b/middleware/cache/handler.go @@ -21,7 +21,7 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( do := state.Do() // might need more from OPT record? - if i, ok := c.Get(qname, qtype, do); ok { + if i, ok := c.get(qname, qtype, do); ok { resp := i.toMsg(r) state.SizeAndDo(resp) w.WriteMsg(resp) @@ -35,12 +35,13 @@ func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ( return c.Next.ServeDNS(ctx, crr, r) } -func (c Cache) Get(qname string, qtype uint16, do bool) (*item, bool) { +func (c Cache) get(qname string, qtype uint16, do bool) (*item, bool) { nxdomain := nameErrorKey(qname, do) if i, ok := c.cache.Get(nxdomain); ok { return i.(*item), true } + // TODO(miek): delegation was added double check successOrNoData := successKey(qname, qtype, do) if i, ok := c.cache.Get(successOrNoData); ok { return i.(*item), true diff --git a/middleware/classify.go b/middleware/classify.go new file mode 100644 index 000000000..72c131157 --- /dev/null +++ b/middleware/classify.go @@ -0,0 +1,52 @@ +package middleware + +import "github.com/miekg/dns" + +type MsgType int + +const ( + Success MsgType = iota + NameError // NXDOMAIN in header, SOA in auth. + NoData // NOERROR in header, SOA in auth. + Delegation // NOERROR in header, NS in auth, optionally fluff in additional (not checked). + OtherError // Don't cache these. +) + +// Classify classifies a message, it returns the MessageType. +func Classify(m *dns.Msg) (MsgType, *dns.OPT) { + opt := m.IsEdns0() + + if len(m.Answer) > 0 && m.Rcode == dns.RcodeSuccess { + return Success, opt + } + + soa := false + ns := 0 + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeSOA { + soa = true + continue + } + if r.Header().Rrtype == dns.TypeNS { + ns++ + } + } + + // Check length of different sections, and drop stuff that is just to large? TODO(miek). + if soa && m.Rcode == dns.RcodeSuccess { + return NoData, opt + } + if soa && m.Rcode == dns.RcodeNameError { + return NameError, opt + } + + if ns > 0 && ns == len(m.Ns) && m.Rcode == dns.RcodeSuccess { + return Delegation, opt + } + + if m.Rcode == dns.RcodeSuccess { + return Success, opt + } + + return OtherError, opt +} diff --git a/middleware/classify_test.go b/middleware/classify_test.go new file mode 100644 index 000000000..26c52db55 --- /dev/null +++ b/middleware/classify_test.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "testing" + + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestClassifyDelegation(t *testing.T) { + m := delegationMsg() + mt, _ := Classify(m) + if mt != Delegation { + t.Errorf("message is wrongly classified, expected delegation, got %d", mt) + } +} + +func delegationMsg() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("miek.nl. 3600 IN NS linode.atoom.net."), + test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 3600 IN NS omval.tednet.nl."), + }, + Extra: []dns.RR{ + test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"), + test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"), + }, + } +} diff --git a/middleware/dnssec/README.md b/middleware/dnssec/README.md new file mode 100644 index 000000000..df00866cf --- /dev/null +++ b/middleware/dnssec/README.md @@ -0,0 +1,35 @@ +# dnssec + +`dnssec` enables on-the-fly DNSSEC signing of served data. + +## Syntax + +~~~ +dnssec [zones...] +~~~ + +* `zones` zones that should be signed. If empty the zones from the configuration block + are used. + +If keys are not specified (see below) a key is generated and used for all signing operations. The +DNSSEC signing will treat this key a CSK (common signing key) forgoing the ZSK/KSK split. All +signing operations are done online. Authenticated denial of existence is implemented with NSEC black +lies. Using ECDSA as an algorithm is preferred as this leads to smaller signatures (compared to +RSA). + +A signing key can be specified by using the `key` directive. + +TODO(miek): think about key rollovers. + + +~~~ +dnssec [zones... ] { + key file [key...] +} +~~~ + +* `key file` indicates key file(s) should be read from disk. When multiple keys are specified, RRset + will be signed with all keys. Generating a key can be done with `dnssec-keygen`: `dnssec-keygen -a + ECDSAP256SHA256 <zonename>`. A key created for zone *A* can be safely used for zone *B*. + +## Examples diff --git a/middleware/dnssec/black_lies.go b/middleware/dnssec/black_lies.go new file mode 100644 index 000000000..527b2fc3e --- /dev/null +++ b/middleware/dnssec/black_lies.go @@ -0,0 +1,24 @@ +package dnssec + +import "github.com/miekg/dns" + +// nsec returns an NSEC useful for NXDOMAIN respsones. +// See https://tools.ietf.org/html/draft-valsorda-dnsop-black-lies-00 +// For example, a request for the non-existing name a.example.com would +// cause the following NSEC record to be generated: +// a.example.com. 3600 IN NSEC \000.a.example.com. ( RRSIG NSEC ) +// This inturn makes every NXDOMAIN answer a NODATA one, don't forget to flip +// the header rcode to NOERROR. +func (d Dnssec) nsec(name, zone string, ttl, incep, expir uint32) ([]dns.RR, error) { + nsec := &dns.NSEC{} + nsec.Hdr = dns.RR_Header{Name: name, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNSEC} + nsec.NextDomain = "\\000." + name + nsec.TypeBitMap = []uint16{dns.TypeRRSIG, dns.TypeNSEC} + + sigs, err := d.sign([]dns.RR{nsec}, zone, ttl, incep, expir) + if err != nil { + return nil, err + } + + return append(sigs, nsec), nil +} diff --git a/middleware/dnssec/black_lies_test.go b/middleware/dnssec/black_lies_test.go new file mode 100644 index 000000000..951e8952e --- /dev/null +++ b/middleware/dnssec/black_lies_test.go @@ -0,0 +1,50 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestZoneSigningBlackLies(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testNxdomainMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Ns, 2) { + t.Errorf("authority section should have 2 sig") + } + var nsec *dns.NSEC + for _, r := range m.Ns { + if r.Header().Rrtype == dns.TypeNSEC { + nsec = r.(*dns.NSEC) + } + } + if m.Rcode != dns.RcodeSuccess { + t.Errorf("expected rcode %d, got %d", dns.RcodeSuccess, m.Rcode) + } + if nsec == nil { + t.Fatalf("expected NSEC, got none") + } + if nsec.Hdr.Name != "ww.miek.nl." { + t.Errorf("expected %s, got %s", "ww.miek.nl.", nsec.Hdr.Name) + } + if nsec.NextDomain != "\\000.ww.miek.nl." { + t.Errorf("expected %s, got %s", "\\000.ww.miek.nl.", nsec.NextDomain) + } + t.Logf("%+v\n", m) +} + +func testNxdomainMsg() *dns.Msg { + return &dns.Msg{MsgHdr: dns.MsgHdr{Rcode: dns.RcodeNameError}, + Question: []dns.Question{dns.Question{Name: "ww.miek.nl.", Qclass: dns.ClassINET, Qtype: dns.TypeTXT}}, + Ns: []dns.RR{test.SOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1461471181 14400 3600 604800 14400")}, + } +} diff --git a/middleware/dnssec/cache.go b/middleware/dnssec/cache.go new file mode 100644 index 000000000..2153c84cb --- /dev/null +++ b/middleware/dnssec/cache.go @@ -0,0 +1,23 @@ +package dnssec + +import ( + "hash/fnv" + "strconv" + + "github.com/miekg/dns" +) + +// Key serializes the RRset and return a signature cache key. +func key(rrs []dns.RR) string { + h := fnv.New64() + buf := make([]byte, 256) + for _, r := range rrs { + off, err := dns.PackRR(r, buf, 0, nil, false) + if err == nil { + h.Write(buf[:off]) + } + } + + i := h.Sum64() + return strconv.FormatUint(i, 10) +} diff --git a/middleware/dnssec/cache_test.go b/middleware/dnssec/cache_test.go new file mode 100644 index 000000000..0039586d5 --- /dev/null +++ b/middleware/dnssec/cache_test.go @@ -0,0 +1,32 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" +) + +func TestCacheSet(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + defer rmPriv() + defer rmPub() + + dnskey, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + + m := testMsg() + state := middleware.State{Req: m} + k := key(m.Answer) // calculate *before* we add the sig + d := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, nil) + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + + _, ok := d.get(k) + if !ok { + t.Errorf("signature was not added to the cache") + } +} diff --git a/middleware/dnssec/dnskey.go b/middleware/dnssec/dnskey.go new file mode 100644 index 000000000..9ae437c54 --- /dev/null +++ b/middleware/dnssec/dnskey.go @@ -0,0 +1,71 @@ +package dnssec + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "errors" + "os" + "time" + + "github.com/miekg/coredns/middleware" + + "github.com/miekg/dns" +) + +type DNSKEY struct { + K *dns.DNSKEY + s crypto.Signer + keytag uint16 +} + +// ParseKeyFile read a DNSSEC keyfile as generated by dnssec-keygen or other +// utilities. It adds ".key" for the public key and ".private" for the private key. +func ParseKeyFile(pubFile, privFile string) (*DNSKEY, error) { + f, e := os.Open(pubFile) + if e != nil { + return nil, e + } + k, e := dns.ReadRR(f, pubFile) + if e != nil { + return nil, e + } + + f, e = os.Open(privFile) + if e != nil { + return nil, e + } + p, e := k.(*dns.DNSKEY).ReadPrivateKey(f, privFile) + if e != nil { + return nil, e + } + + if v, ok := p.(*rsa.PrivateKey); ok { + return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil + } + if v, ok := p.(*ecdsa.PrivateKey); ok { + return &DNSKEY{k.(*dns.DNSKEY), v, k.(*dns.DNSKEY).KeyTag()}, nil + } + return &DNSKEY{k.(*dns.DNSKEY), nil, 0}, errors.New("no known? private key found") +} + +// getDNSKEY returns the correct DNSKEY to the client. Signatures are added when do is true. +func (d Dnssec) getDNSKEY(state middleware.State, zone string, do bool) *dns.Msg { + keys := make([]dns.RR, len(d.keys)) + for i, k := range d.keys { + keys[i] = dns.Copy(k.K) + keys[i].Header().Name = zone + } + m := new(dns.Msg) + m.SetReply(state.Req) + m.Answer = keys + if !do { + return m + } + + incep, expir := incepExpir(time.Now().UTC()) + if sigs, err := d.sign(keys, zone, 3600, incep, expir); err == nil { + m.Answer = append(m.Answer, sigs...) + } + return m +} diff --git a/middleware/dnssec/dnssec.go b/middleware/dnssec/dnssec.go new file mode 100644 index 000000000..b0a328bee --- /dev/null +++ b/middleware/dnssec/dnssec.go @@ -0,0 +1,127 @@ +package dnssec + +import ( + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/singleflight" + + "github.com/miekg/dns" + gcache "github.com/patrickmn/go-cache" +) + +type Dnssec struct { + Next middleware.Handler + zones []string + keys []*DNSKEY + inflight *singleflight.Group + cache *gcache.Cache +} + +func NewDnssec(zones []string, keys []*DNSKEY, next middleware.Handler) Dnssec { + return Dnssec{Next: next, + zones: zones, + keys: keys, + cache: gcache.New(defaultDuration, purgeDuration), + inflight: new(singleflight.Group), + } +} + +// Sign signs the message m. it takes care of negative or nodata responses. It +// uses NSEC black lies for authenticated denial of existence. Signatures +// creates will be cached for a short while. By default we sign for 8 days, +// starting 3 hours ago. +func (d Dnssec) Sign(state middleware.State, zone string, now time.Time) *dns.Msg { + req := state.Req + mt, _ := middleware.Classify(req) // TODO(miek): need opt record here? + if mt == middleware.Delegation { + return req + } + + incep, expir := incepExpir(now) + + if mt == middleware.NameError { + if req.Ns[0].Header().Rrtype != dns.TypeSOA || len(req.Ns) > 1 { + return req + } + + ttl := req.Ns[0].Header().Ttl + + if sigs, err := d.sign(req.Ns, zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if sigs, err := d.nsec(state.Name(), zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + if len(req.Ns) > 1 { // actually added nsec and sigs, reset the rcode + req.Rcode = dns.RcodeSuccess + } + return req + } + + for _, r := range rrSets(req.Answer) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Answer = append(req.Answer, sigs...) + } + } + for _, r := range rrSets(req.Ns) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Ns = append(req.Ns, sigs...) + } + } + for _, r := range rrSets(req.Extra) { + ttl := r[0].Header().Ttl + if sigs, err := d.sign(r, zone, ttl, incep, expir); err == nil { + req.Extra = append(req.Extra, sigs...) + } + } + return req +} + +func (d Dnssec) sign(rrs []dns.RR, signerName string, ttl, incep, expir uint32) ([]dns.RR, error) { + k := key(rrs) + sgs, ok := d.get(k) + if ok { + return sgs, nil + } + + sigs, err := d.inflight.Do(k, func() (interface{}, error) { + sigs := make([]dns.RR, len(d.keys)) + var e error + for i, k := range d.keys { + sig := k.NewRRSIG(signerName, ttl, incep, expir) + e = sig.Sign(k.s, rrs) + sigs[i] = sig + } + d.set(k, sigs) + return sigs, e + }) + return sigs.([]dns.RR), err +} + +func (d Dnssec) set(key string, sigs []dns.RR) { + // we insert the sigs with a duration that is 24 hours less then the expiration, as these + // sigs have *just* been made the duration is 7 days. + d.cache.Set(key, sigs, eightDays-24*time.Hour) +} + +func (d Dnssec) get(key string) ([]dns.RR, bool) { + if s, ok := d.cache.Get(key); ok { + return s.([]dns.RR), true + } + return nil, false +} + +func incepExpir(now time.Time) (uint32, uint32) { + incep := uint32(now.Add(-3 * time.Hour).Unix()) // -(2+1) hours, be sure to catch daylight saving time and such + expir := uint32(now.Add(eightDays).Unix()) // sign for 8 days + return incep, expir +} + +const ( + purgeDuration = 3 * time.Hour + defaultDuration = 24 * time.Hour + eightDays = 8 * 24 * time.Hour +) diff --git a/middleware/dnssec/dnssec_test.go b/middleware/dnssec/dnssec_test.go new file mode 100644 index 000000000..49b0d5d3a --- /dev/null +++ b/middleware/dnssec/dnssec_test.go @@ -0,0 +1,193 @@ +package dnssec + +import ( + "testing" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" +) + +func TestZoneSigning(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsg() + state := middleware.State{Req: m} + + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 1) { + t.Errorf("authority section should have 1 sig") + } +} + +func TestZoneSigningDouble(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + fPriv1, rmPriv1, _ := test.TempFile(t, ".", privKey1) + fPub1, rmPub1, _ := test.TempFile(t, ".", pubKey1) + defer rmPriv1() + defer rmPub1() + + key1, err := ParseKeyFile(fPub1, fPriv1) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + d.keys = append(d.keys, key1) + + m := testMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 2) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 2) { + t.Errorf("authority section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +// TestSigningDifferentZone tests if a key for miek.nl and be used for example.org. +func TestSigningDifferentZone(t *testing.T) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + defer rmPriv() + defer rmPub() + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + + m := testMsgEx() + state := middleware.State{Req: m} + d := NewDnssec([]string{"example.org."}, []*DNSKEY{key}, nil) + m = d.Sign(state, "example.org.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + if !section(m.Ns, 1) { + t.Errorf("authority section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +func TestSigningCname(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testMsgCname() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Answer, 1) { + t.Errorf("answer section should have 1 sig") + } + t.Logf("%+v\n", m) +} + +func TestZoneSigningDelegation(t *testing.T) { + d, rm1, rm2 := newDnssec(t, []string{"miek.nl."}) + defer rm1() + defer rm2() + + m := testDelegationMsg() + state := middleware.State{Req: m} + m = d.Sign(state, "miek.nl.", time.Now().UTC()) + if !section(m.Ns, 0) { + t.Errorf("authority section should have 0 sig") + t.Logf("%v\n", m) + } + if !section(m.Extra, 0) { + t.Errorf("answer section should have 0 sig") + t.Logf("%v\n", m) + } +} + +func section(rss []dns.RR, nrSigs int) bool { + i := 0 + for _, r := range rss { + if r.Header().Rrtype == dns.TypeRRSIG { + i++ + } + } + return nrSigs == i +} + +func testMsg() *dns.Msg { + // don't care about the message header + return &dns.Msg{ + Answer: []dns.RR{test.MX("miek.nl. 1703 IN MX 1 aspmx.l.google.com.")}, + Ns: []dns.RR{test.NS("miek.nl. 1703 IN NS omval.tednet.nl.")}, + } +} +func testMsgEx() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{test.MX("example.org. 1703 IN MX 1 aspmx.l.google.com.")}, + Ns: []dns.RR{test.NS("example.org. 1703 IN NS omval.tednet.nl.")}, + } +} + +func testMsgCname() *dns.Msg { + return &dns.Msg{ + Answer: []dns.RR{test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl.")}, + } +} + +func testDelegationMsg() *dns.Msg { + return &dns.Msg{ + Ns: []dns.RR{ + test.NS("miek.nl. 3600 IN NS linode.atoom.net."), + test.NS("miek.nl. 3600 IN NS ns-ext.nlnetlabs.nl."), + test.NS("miek.nl. 3600 IN NS omval.tednet.nl."), + }, + Extra: []dns.RR{ + test.A("omval.tednet.nl. 3600 IN A 185.49.141.42"), + test.AAAA("omval.tednet.nl. 3600 IN AAAA 2a04:b900:0:100::42"), + }, + } +} + +func newDnssec(t *testing.T, zones []string) (Dnssec, func(), func()) { + k, rm1, rm2 := newKey(t) + d := NewDnssec(zones, []*DNSKEY{k}, nil) + return d, rm1, rm2 +} + +func newKey(t *testing.T) (*DNSKEY, func(), func()) { + fPriv, rmPriv, _ := test.TempFile(t, ".", privKey) + fPub, rmPub, _ := test.TempFile(t, ".", pubKey) + + key, err := ParseKeyFile(fPub, fPriv) + if err != nil { + t.Fatalf("failed to parse key: %v\n", err) + } + return key, rmPriv, rmPub +} + +const ( + pubKey = `miek.nl. IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4BXPP3gwhetiOUMnGA+x09nqzgF5IY OyjWB7N3rXqQbnOSILhH1hnuyh7mmA==` + privKey = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: /4BZk8AFvyW5hL3cOLSVxIp1RTqHSAEloWUxj86p3gs= +Created: 20160423195532 +Publish: 20160423195532 +Activate: 20160423195532 +` + pubKey1 = `example.org. IN DNSKEY 257 3 13 tVRWNSGpHZbCi7Pr7OmbADVUO3MxJ0Lb8Lk3o/HBHqCxf5K/J50lFqRa 98lkdAIiFOVRy8LyMvjwmxZKwB5MNw==` + privKey1 = `Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i8j4OfDGT8CQt24SDwLz2hg9yx4qKOEOh1LvbAuSp1c= +Created: 20160423211746 +Publish: 20160423211746 +Activate: 20160423211746 +` +) diff --git a/middleware/dnssec/handler.go b/middleware/dnssec/handler.go new file mode 100644 index 000000000..0c0f4a61d --- /dev/null +++ b/middleware/dnssec/handler.go @@ -0,0 +1,61 @@ +package dnssec + +import ( + "github.com/miekg/coredns/middleware" + + "github.com/miekg/dns" + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/net/context" +) + +// ServeDNS implements the middleware.Handler interface. +func (d Dnssec) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := middleware.State{W: w, Req: r} + + do := state.Do() + qname := state.Name() + qtype := state.QType() + zone := middleware.Zones(d.zones).Matches(qname) + if zone == "" { + return d.Next.ServeDNS(ctx, w, r) + } + + // Intercept queries for DNSKEY, but only if one of the zones matches the qname, otherwise we let + // the query through. + if qtype == dns.TypeDNSKEY { + for _, z := range d.zones { + if qname == z { + resp := d.getDNSKEY(state, z, do) + state.SizeAndDo(resp) + w.WriteMsg(resp) + return dns.RcodeSuccess, nil + } + } + } + + drr := NewDnssecResponseWriter(w, d) + return d.Next.ServeDNS(ctx, drr, r) +} + +var ( + cacheHitCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: middleware.Namespace, + Subsystem: subsystem, + Name: "hit_count_total", + Help: "Counter of signatures that were found in the cache.", + }, []string{"zone"}) + + cacheMissCount = prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: middleware.Namespace, + Subsystem: subsystem, + Name: "miss_count_total", + Help: "Counter of signatures that were not found in the cache.", + }, []string{"zone"}) +) + +const subsystem = "dnssec" + +func init() { + prometheus.MustRegister(cacheHitCount) + prometheus.MustRegister(cacheMissCount) +} diff --git a/middleware/dnssec/handler_test.go b/middleware/dnssec/handler_test.go new file mode 100644 index 000000000..9e6cedf8a --- /dev/null +++ b/middleware/dnssec/handler_test.go @@ -0,0 +1,170 @@ +package dnssec + +import ( + "sort" + "strings" + "testing" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/coredns/middleware/file" + "github.com/miekg/coredns/middleware/test" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +var dnssecTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, Do: true, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + test.RRSIG("miek.nl. 3600 IN RRSIG DNSKEY 13 2 3600 20160503150844 20160425120844 18512 miek.nl. Iw/kNOyM"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, +} + +var dnsTestCases = []test.Case{ + { + Qname: "miek.nl.", Qtype: dns.TypeDNSKEY, + Answer: []dns.RR{ + test.DNSKEY("miek.nl. 3600 IN DNSKEY 257 3 13 0J8u0XJ9GNGFEBXuAmLu04taHG4"), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + }, + }, + { + Qname: "miek.nl.", Qtype: dns.TypeMX, Do: true, + Answer: []dns.RR{ + test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."), + test.RRSIG("miek.nl. 1800 IN RRSIG MX 13 2 3600 20160503192428 20160425162428 18512 miek.nl. 4nxuGKitXjPVA9zP1JIUvA09"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, + { + Qname: "www.miek.nl.", Qtype: dns.TypeAAAA, Do: true, + Answer: []dns.RR{ + test.AAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"), + test.RRSIG("a.miek.nl. 1800 IN RRSIG AAAA 13 3 3600 20160503193047 20160425163047 18512 miek.nl. UAyMG+gcnoXW3"), + test.CNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."), + test.RRSIG("www.miek.nl. 1800 IN RRSIG CNAME 13 3 3600 20160503193047 20160425163047 18512 miek.nl. E3qGZn"), + }, + Extra: []dns.RR{test.OPT(4096, true)}, + }, + { + Qname: "www.example.org.", Qtype: dns.TypeAAAA, Do: true, + Rcode: dns.RcodeServerFailure, + // Extra: []dns.RR{test.OPT(4096, true)}, // test.ErrorHandler is a simple handler that does not do EDNS. + }, +} + +func TestLookupZone(t *testing.T) { + zone, err := file.Parse(strings.NewReader(dbMiekNL), "miek.nl.", "stdin") + if err != nil { + return + } + fm := file.File{Next: test.ErrorHandler(), Zones: file.Zones{Z: map[string]*file.Zone{"miek.nl.": zone}, Names: []string{"miek.nl."}}} + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, fm) + ctx := context.TODO() + + for _, tc := range dnsTestCases { + m := tc.Msg() + + rec := middleware.NewResponseRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("expected no error, got %v\n", err) + return + } + resp := rec.Msg() + + sort.Sort(test.RRSet(resp.Answer)) + sort.Sort(test.RRSet(resp.Ns)) + sort.Sort(test.RRSet(resp.Extra)) + + if !test.Header(t, tc, resp) { + t.Logf("%v\n", resp) + continue + } + if !test.Section(t, tc, test.Answer, resp.Answer) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Ns, resp.Ns) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Extra, resp.Extra) { + t.Logf("%v\n", resp) + } + } +} + +func TestLookupDNSKEY(t *testing.T) { + dnskey, rm1, rm2 := newKey(t) + defer rm1() + defer rm2() + dh := NewDnssec([]string{"miek.nl."}, []*DNSKEY{dnskey}, test.ErrorHandler()) + ctx := context.TODO() + + for _, tc := range dnssecTestCases { + m := tc.Msg() + + rec := middleware.NewResponseRecorder(&test.ResponseWriter{}) + _, err := dh.ServeDNS(ctx, rec, m) + if err != nil { + t.Errorf("expected no error, got %v\n", err) + return + } + resp := rec.Msg() + + sort.Sort(test.RRSet(resp.Answer)) + sort.Sort(test.RRSet(resp.Ns)) + sort.Sort(test.RRSet(resp.Extra)) + + if !test.Header(t, tc, resp) { + t.Logf("%v\n", resp) + continue + } + if !test.Section(t, tc, test.Answer, resp.Answer) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Ns, resp.Ns) { + t.Logf("%v\n", resp) + } + if !test.Section(t, tc, test.Extra, resp.Extra) { + t.Logf("%v\n", resp) + } + } +} + +const dbMiekNL = ` +$TTL 30M +$ORIGIN miek.nl. +@ IN SOA linode.atoom.net. miek.miek.nl. ( + 1282630057 ; Serial + 4H ; Refresh + 1H ; Retry + 7D ; Expire + 4H ) ; Negative Cache TTL + IN NS linode.atoom.net. + + IN MX 1 aspmx.l.google.com. + + IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 + +a IN A 139.162.196.78 + IN AAAA 2a01:7e00::f03c:91ff:fef1:6735 +www IN CNAME a` diff --git a/middleware/dnssec/responsewriter.go b/middleware/dnssec/responsewriter.go new file mode 100644 index 000000000..2a7cbb972 --- /dev/null +++ b/middleware/dnssec/responsewriter.go @@ -0,0 +1,48 @@ +package dnssec + +import ( + "log" + "time" + + "github.com/miekg/coredns/middleware" + "github.com/miekg/dns" +) + +type DnssecResponseWriter struct { + dns.ResponseWriter + d Dnssec +} + +func NewDnssecResponseWriter(w dns.ResponseWriter, d Dnssec) *DnssecResponseWriter { + return &DnssecResponseWriter{w, d} +} + +func (d *DnssecResponseWriter) WriteMsg(res *dns.Msg) error { + // By definition we should sign anything that comes back, we should still figure out for + // which zone it should be. + state := middleware.State{W: d.ResponseWriter, Req: res} + + qname := state.Name() + zone := middleware.Zones(d.d.zones).Matches(qname) + if zone == "" { + return d.ResponseWriter.WriteMsg(res) + } + + if state.Do() { + res = d.d.Sign(state, zone, time.Now().UTC()) + } + state.SizeAndDo(res) + + return d.ResponseWriter.WriteMsg(res) +} + +func (d *DnssecResponseWriter) Write(buf []byte) (int, error) { + log.Printf("[WARNING] Dnssec called with Write: not signing reply") + n, err := d.ResponseWriter.Write(buf) + return n, err +} + +func (d *DnssecResponseWriter) Hijack() { + d.ResponseWriter.Hijack() + return +} diff --git a/middleware/dnssec/rrsig.go b/middleware/dnssec/rrsig.go new file mode 100644 index 000000000..17bc1195b --- /dev/null +++ b/middleware/dnssec/rrsig.go @@ -0,0 +1,53 @@ +package dnssec + +import "github.com/miekg/dns" + +// newRRSIG return a new RRSIG, with all fields filled out, except the signed data. +func (k *DNSKEY) NewRRSIG(signerName string, ttl, incep, expir uint32) *dns.RRSIG { + sig := new(dns.RRSIG) + + sig.Hdr.Rrtype = dns.TypeRRSIG + sig.Algorithm = k.K.Algorithm + sig.KeyTag = k.keytag + sig.SignerName = signerName + sig.Hdr.Ttl = ttl + sig.OrigTtl = origTtl + + sig.Inception = incep + sig.Expiration = expir + + return sig +} + +type rrset struct { + qname string + qtype uint16 +} + +// rrSets returns rrs as a map of RRsets. It skips RRSIG and OPT records as those don't need to be signed. +func rrSets(rrs []dns.RR) map[rrset][]dns.RR { + m := make(map[rrset][]dns.RR) + + for _, r := range rrs { + if r.Header().Rrtype == dns.TypeRRSIG || r.Header().Rrtype == dns.TypeOPT { + continue + } + + if s, ok := m[rrset{r.Header().Name, r.Header().Rrtype}]; ok { + s = append(s, r) + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + continue + } + + s := make([]dns.RR, 1, 3) + s[0] = r + m[rrset{r.Header().Name, r.Header().Rrtype}] = s + } + + if len(m) > 0 { + return m + } + return nil +} + +const origTtl = 3600 diff --git a/middleware/etcd/etcd.go b/middleware/etcd/etcd.go index 4eb55f7ed..38eac7ab3 100644 --- a/middleware/etcd/etcd.go +++ b/middleware/etcd/etcd.go @@ -8,8 +8,8 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd/msg" - "github.com/miekg/coredns/middleware/etcd/singleflight" "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/singleflight" etcdc "github.com/coreos/etcd/client" "golang.org/x/net/context" diff --git a/middleware/etcd/lookup.go b/middleware/etcd/lookup.go index 88f362fc7..35d4b7226 100644 --- a/middleware/etcd/lookup.go +++ b/middleware/etcd/lookup.go @@ -317,11 +317,14 @@ func (e Etcd) NS(zone string, state middleware.State) (records, extra []dns.RR, // NS record for this zone live in a special place, ns.dns.<zone>. Fake our lookup. // only a tad bit fishy... old := state.QName() + + state.Clear() state.Req.Question[0].Name = "ns.dns." + zone services, err := e.records(state, false) if err != nil { return nil, nil, err } + // ... and reset state.Req.Question[0].Name = old for _, serv := range services { diff --git a/middleware/etcd/setup_test.go b/middleware/etcd/setup_test.go index b28602122..a695d43ce 100644 --- a/middleware/etcd/setup_test.go +++ b/middleware/etcd/setup_test.go @@ -10,8 +10,8 @@ import ( "github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware/etcd/msg" - "github.com/miekg/coredns/middleware/etcd/singleflight" "github.com/miekg/coredns/middleware/proxy" + "github.com/miekg/coredns/middleware/singleflight" "github.com/miekg/coredns/middleware/test" "github.com/miekg/dns" diff --git a/middleware/file/closest.go b/middleware/file/closest.go index 6af033e64..741327fde 100644 --- a/middleware/file/closest.go +++ b/middleware/file/closest.go @@ -52,6 +52,10 @@ func (z *Zone) nameErrorProof(qname string, qtype uint16) []dns.RR { } } + if len(nsec) == 0 || len(nsec1) == 0 { + return nsec + } + // Check for duplicate NSEC. if nsec[nsecIndex].Header().Name == nsec1[nsec1Index].Header().Name && nsec[nsecIndex].(*dns.NSEC).NextDomain == nsec1[nsec1Index].(*dns.NSEC).NextDomain { diff --git a/middleware/file/file.go b/middleware/file/file.go index 441e8b94d..a99a64d6f 100644 --- a/middleware/file/file.go +++ b/middleware/file/file.go @@ -1,7 +1,7 @@ package file import ( - "fmt" + "errors" "io" "log" @@ -27,12 +27,15 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i state := middleware.State{W: w, Req: r} if state.QClass() != dns.ClassINET { - return dns.RcodeServerFailure, fmt.Errorf("can only deal with ClassINET") + return dns.RcodeServerFailure, errors.New("can only deal with ClassINET") } qname := state.Name() zone := middleware.Zones(f.Zones.Names).Matches(qname) if zone == "" { - return f.Next.ServeDNS(ctx, w, r) + if f.Next != nil { + return f.Next.ServeDNS(ctx, w, r) + } + return dns.RcodeServerFailure, errors.New("no next middleware found") } z, ok := f.Zones.Z[zone] if !ok { diff --git a/middleware/file/reload_test.go b/middleware/file/reload_test.go index 1769c701e..1ba9f4bcf 100644 --- a/middleware/file/reload_test.go +++ b/middleware/file/reload_test.go @@ -11,7 +11,7 @@ import ( ) func TestZoneReload(t *testing.T) { - fileName, rm, err := test.Zone(t, ".", reloadZoneTest) + fileName, rm, err := test.TempFile(t, ".", reloadZoneTest) if err != nil { t.Fatalf("failed to create zone: %s", err) } diff --git a/middleware/etcd/singleflight/singleflight.go b/middleware/singleflight/singleflight.go index ff2c2ee4f..ff2c2ee4f 100644 --- a/middleware/etcd/singleflight/singleflight.go +++ b/middleware/singleflight/singleflight.go diff --git a/middleware/state.go b/middleware/state.go index ec2b618a6..34e6cdfc3 100644 --- a/middleware/state.go +++ b/middleware/state.go @@ -15,9 +15,13 @@ type State struct { Req *dns.Msg W dns.ResponseWriter - // Cache size after first call to Size or Do + // Cache size after first call to Size or Do. size int do int // 0: not, 1: true: 2: false + // TODO(miek): opt record itself as well. + + // Cache name as (lowercase) well + name string } // Now returns the current timestamp in the specified format. @@ -26,12 +30,6 @@ func (s *State) Now(format string) string { return time.Now().Format(format) } // NowDate returns the current date/time that can be used in other time functions. func (s *State) NowDate() time.Time { return time.Now() } -// Header gets the heaser of the request in State. -func (s *State) Header() *dns.RR_Header { - // TODO(miek) - return nil -} - // IP gets the (remote) IP address of the client making the request. func (s *State) IP() string { ip, _, err := net.SplitHostPort(s.W.RemoteAddr().String()) @@ -191,7 +189,13 @@ func (s *State) QType() uint16 { return s.Req.Question[0].Qtype } // Name returns the name of the question in the request. Note // this name will always have a closing dot and will be lower cased. -func (s *State) Name() string { return strings.ToLower(dns.Name(s.Req.Question[0].Name).String()) } +func (s *State) Name() string { + if s.name != "" { + return s.name + } + s.name = strings.ToLower(dns.Name(s.Req.Question[0].Name).String()) + return s.name +} // QName returns the name of the question in the request. func (s *State) QName() string { return dns.Name(s.Req.Question[0].Name).String() } @@ -210,6 +214,11 @@ func (s *State) ErrorMessage(rcode int) *dns.Msg { return m } +// Clear clears all caching from State s. +func (s *State) Clear() { + s.name = "" +} + const ( doTrue = 1 doFalse = 2 diff --git a/middleware/test/file.go b/middleware/test/file.go new file mode 100644 index 000000000..b6068a32b --- /dev/null +++ b/middleware/test/file.go @@ -0,0 +1,20 @@ +package test + +import ( + "io/ioutil" + "os" + "testing" +) + +// TempFile will create a temporary file on disk and returns the name and a cleanup function to remove it later. +func TempFile(t *testing.T, dir, content string) (string, func(), error) { + f, err := ioutil.TempFile(dir, "go-test-tmpfile") + if err != nil { + return "", nil, err + } + if err := ioutil.WriteFile(f.Name(), []byte(content), 0644); err != nil { + return "", nil, err + } + rmFunc := func() { os.Remove(f.Name()) } + return f.Name(), rmFunc, nil +} diff --git a/middleware/test/helpers.go b/middleware/test/helpers.go index 3096f126b..a01d7a306 100644 --- a/middleware/test/helpers.go +++ b/middleware/test/helpers.go @@ -45,17 +45,18 @@ func (c Case) Msg() *dns.Msg { return m } -func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } -func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } -func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } -func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } -func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } -func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } -func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } -func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } -func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } -func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } -func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } +func A(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) } +func AAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) } +func CNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) } +func SRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) } +func SOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) } +func NS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) } +func PTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) } +func TXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) } +func MX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) } +func RRSIG(rr string) *dns.RRSIG { r, _ := dns.NewRR(rr); return r.(*dns.RRSIG) } +func NSEC(rr string) *dns.NSEC { r, _ := dns.NewRR(rr); return r.(*dns.NSEC) } +func DNSKEY(rr string) *dns.DNSKEY { r, _ := dns.NewRR(rr); return r.(*dns.DNSKEY) } func OPT(bufsize int, do bool) *dns.OPT { o := new(dns.OPT) diff --git a/middleware/test/zone.go b/middleware/test/zone.go deleted file mode 100644 index 490280a7a..000000000 --- a/middleware/test/zone.go +++ /dev/null @@ -1,21 +0,0 @@ -package test - -import ( - "io/ioutil" - "os" - "testing" -) - -// Zone will create a temporary file on disk and returns the name and -// cleanup function to remove it later. -func Zone(t *testing.T, dir, zonefile string) (string, func(), error) { - f, err := ioutil.TempFile(dir, "go-test-zone") - if err != nil { - return "", nil, err - } - if err := ioutil.WriteFile(f.Name(), []byte(zonefile), 0644); err != nil { - return "", nil, err - } - rmFunc := func() { os.Remove(f.Name()) } - return f.Name(), rmFunc, nil -} |