Commit 5387c162 authored by Miek Gieben's avatar Miek Gieben

Implement a DNS zone

Full implementation, DNS (and in the future DNSSEC). Returns answer in a
hopefully standards compliant way.
Testing with my miek.nl zone are included as well.
This should correctly handle nodata, nxdomain and cnames.
parent 9eeb2b02
package setup package setup
import ( import (
"log"
"os" "os"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/file" "github.com/miekg/coredns/middleware/file"
"github.com/miekg/dns"
) )
// File sets up the file middleware. // File sets up the file middleware.
...@@ -23,8 +20,7 @@ func File(c *Controller) (middleware.Middleware, error) { ...@@ -23,8 +20,7 @@ func File(c *Controller) (middleware.Middleware, error) {
} }
func fileParse(c *Controller) (file.Zones, error) { func fileParse(c *Controller) (file.Zones, error) {
// Maybe multiple, each for each zone. z := make(map[string]*file.Zone)
z := make(map[string]file.Zone)
names := []string{} names := []string{}
for c.Next() { for c.Next() {
if c.Val() == "file" { if c.Val() == "file" {
...@@ -42,7 +38,12 @@ func fileParse(c *Controller) (file.Zones, error) { ...@@ -42,7 +38,12 @@ func fileParse(c *Controller) (file.Zones, error) {
// normalize this origin // normalize this origin
origin = middleware.Host(origin).StandardHost() origin = middleware.Host(origin).StandardHost()
zone, err := parseZone(origin, fileName) reader, err := os.Open(fileName)
if err != nil {
return file.Zones{}, err
}
zone, err := file.Parse(reader, origin, fileName)
if err == nil { if err == nil {
z[origin] = zone z[origin] = zone
} }
...@@ -51,24 +52,3 @@ func fileParse(c *Controller) (file.Zones, error) { ...@@ -51,24 +52,3 @@ func fileParse(c *Controller) (file.Zones, error) {
} }
return file.Zones{Z: z, Names: names}, nil return file.Zones{Z: z, Names: names}, nil
} }
//
// parsrZone parses the zone in filename and returns a []RR or an error.
func parseZone(origin, fileName string) (file.Zone, error) {
f, err := os.Open(fileName)
if err != nil {
return nil, err
}
tokens := dns.ParseZone(f, origin, fileName)
zone := make([]dns.RR, 0, defaultZoneSize)
for x := range tokens {
if x.Error != nil {
log.Printf("[ERROR] failed to parse %s: %v", origin, x.Error)
return nil, x.Error
}
zone = append(zone, x.RR)
}
return file.Zone(zone), nil
}
const defaultZoneSize = 20 // A made up number.
...@@ -14,7 +14,8 @@ func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i ...@@ -14,7 +14,8 @@ func (e Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
return e.Next.ServeDNS(ctx, w, r) return e.Next.ServeDNS(ctx, w, r)
} }
m := state.AnswerMessage() m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
var ( var (
......
...@@ -6,12 +6,13 @@ package file ...@@ -6,12 +6,13 @@ package file
// have some fluff for DNSSEC (and be memory efficient). // have some fluff for DNSSEC (and be memory efficient).
import ( import (
"strings" "io"
"log"
"golang.org/x/net/context"
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context"
) )
type ( type (
...@@ -21,9 +22,8 @@ type ( ...@@ -21,9 +22,8 @@ type (
// Maybe a list of all zones as well, as a []string? // Maybe a list of all zones as well, as a []string?
} }
Zone []dns.RR
Zones struct { Zones struct {
Z map[string]Zone // utterly braindead impl. TODO(miek): fix Z map[string]*Zone
Names []string Names []string
} }
) )
...@@ -35,57 +35,51 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i ...@@ -35,57 +35,51 @@ func (f File) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (i
if zone == "" { if zone == "" {
return f.Next.ServeDNS(ctx, w, r) return f.Next.ServeDNS(ctx, w, r)
} }
z, ok := f.Zones.Z[zone]
if !ok {
return f.Next.ServeDNS(ctx, w, r)
}
names, nodata := f.Zones.Z[zone].lookup(qname, state.QType()) rrs, extra, result := z.Lookup(qname, state.QType(), state.Do())
var answer *dns.Msg
switch { m := new(dns.Msg)
case nodata: m.SetReply(r)
answer = state.AnswerMessage() m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
answer.Ns = names
case len(names) == 0: switch result {
answer = state.AnswerMessage() case Success:
answer.Ns = names // case?
answer.Rcode = dns.RcodeNameError m.Answer = rrs
case len(names) > 0: m.Extra = extra
answer = state.AnswerMessage() // Ns section
answer.Answer = names case NameError:
m.Rcode = dns.RcodeNameError
fallthrough
case NoData:
// case?
m.Ns = rrs
default: default:
answer = state.ErrorMessage(dns.RcodeServerFailure) // TODO
} }
// Check return size, etc. TODO(miek) // sizing and Do bit RRSIG
w.WriteMsg(answer) w.WriteMsg(m)
return 0, nil return dns.RcodeSuccess, nil
} }
// Lookup will try to find qname and qtype in z. It returns the // Parse parses the zone in filename and returns a new Zone or an error.
// records found *or* a boolean saying NODATA. If the answer func Parse(f io.Reader, origin, fileName string) (*Zone, error) {
// is NODATA then the RR returned is the SOA record. tokens := dns.ParseZone(f, dns.Fqdn(origin), fileName)
// z := NewZone(origin)
// TODO(miek): EXTREMELY STUPID IMPLEMENTATION. for x := range tokens {
// Doesn't do much, no delegation, no cname, nothing really, etc. if x.Error != nil {
// TODO(miek): even NODATA looks broken log.Printf("[ERROR] failed to parse %s: %v", origin, x.Error)
func (z Zone) lookup(qname string, qtype uint16) ([]dns.RR, bool) { return nil, x.Error
var (
nodata bool
rep []dns.RR
soa dns.RR
)
for _, rr := range z {
if rr.Header().Rrtype == dns.TypeSOA {
soa = rr
} }
// Match function in Go DNS? if x.RR.Header().Rrtype == dns.TypeSOA {
if strings.ToLower(rr.Header().Name) == qname { z.SOA = x.RR.(*dns.SOA)
if rr.Header().Rrtype == qtype { continue
rep = append(rep, rr)
nodata = false
}
} }
z.Insert(x.RR)
} }
if nodata { return z, nil
return []dns.RR{soa}, true
}
return rep, false
} }
# file
`file` enabled reading zone data from a RFC-1035 styled file.
The etcd middleware makes extensive use of the proxy middleware to forward and query
other servers in the network.
## Syntax
~~~
file dbfile [zones...]
~~~
* `dbfile` the database file to read and parse.
* `zones` zones it should be authoritative for. If empty the zones from the configuration block
are used.
If you want to `round robin` A and AAAA responses look at the `loadbalance` middleware.
~~~
file {
db <dsds>
masters [...masters...]
}
~~~
* `path` /skydns
* `endpoint` endpoints...
* `stubzones`
## Examples
dnssec {
file blaat, transparant allow already signed responses
ksk bliep.dsdsk
}
package file
import "github.com/miekg/dns"
// Result is the result of a Lookup
type Result int
const (
Success Result = iota
NameError
NoData // aint no offical NoData return code.
)
// Lookup looks up qname and qtype in the zone, when do is true DNSSEC are included as well.
// Two sets of records are returned, one for the answer and one for the additional section.
func (z *Zone) Lookup(qname string, qtype uint16, do bool) ([]dns.RR, []dns.RR, Result) {
// TODO(miek): implement DNSSEC
var rr dns.RR
mk, known := dns.TypeToRR[qtype]
if !known {
return nil, nil, NameError
// Uhm...?
// rr = new(RFC3597)
} else {
rr = mk()
}
if qtype == dns.TypeSOA {
return z.lookupSOA(do)
}
rr.Header().Name = qname
elem := z.Tree.Get(rr)
if elem == nil {
return []dns.RR{z.SOA}, nil, NameError
}
rrs := elem.Types(dns.TypeCNAME)
if len(rrs) > 0 { // should only ever be 1 actually; TODO(miek) check for this?
// lookup target from the cname
rr.Header().Name = rrs[0].(*dns.CNAME).Target
elem := z.Tree.Get(rr)
if elem == nil {
return rrs, nil, Success
}
return rrs, elem.All(), Success
}
rrs = elem.Types(qtype)
if len(rrs) == 0 {
return []dns.RR{z.SOA}, nil, NoData
}
// Need to check sub-type on RRSIG records to only include the correctly
// typed ones.
return rrs, nil, Success
}
func (z *Zone) lookupSOA(do bool) ([]dns.RR, []dns.RR, Result) {
return []dns.RR{z.SOA}, nil, Success
}
// signatureForSubType range through the signature and return the correct
// ones for the subtype.
func (z *Zone) signatureForSubType(rrs []dns.RR, subtype uint16, do bool) []dns.RR {
if !do {
return nil
}
sigs := []dns.RR{}
for _, sig := range rrs {
if s, ok := sig.(*dns.RRSIG); ok {
if s.TypeCovered == subtype {
sigs = append(sigs, s)
}
}
}
return sigs
}
package file
import (
"sort"
"strings"
"testing"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
var dnsTestCases = []dnsTestCase{
{
Qname: "miek.nl.", Qtype: dns.TypeSOA,
Answer: []dns.RR{
newSOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeAAAA,
Answer: []dns.RR{
newAAAA("miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
},
},
{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
newMX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
newMX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."),
newMX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
newMX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
newMX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
},
},
{
Qname: "www.miek.nl.", Qtype: dns.TypeA,
Answer: []dns.RR{
newCNAME("www.miek.nl. 1800 IN CNAME a.miek.nl."),
},
Extra: []dns.RR{
newA("a.miek.nl. 1800 IN A 139.162.196.78"),
newAAAA("a.miek.nl. 1800 IN AAAA 2a01:7e00::f03c:91ff:fef1:6735"),
},
},
{
Qname: "a.miek.nl.", Qtype: dns.TypeSRV,
Ns: []dns.RR{
newSOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
},
{
Qname: "b.miek.nl.", Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
newSOA("miek.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1282630057 14400 3600 604800 14400"),
},
},
}
type rrSet []dns.RR
func (p rrSet) Len() int { return len(p) }
func (p rrSet) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p rrSet) Less(i, j int) bool { return p[i].String() < p[j].String() }
const testzone = "miek.nl."
func TestLookup(t *testing.T) {
zone, err := Parse(strings.NewReader(dbMiekNL), testzone, "stdin")
if err != nil {
t.Fatalf("expect no error when reading zone, got %q", err)
}
fm := File{Next: handler(), Zones: Zones{Z: map[string]*Zone{testzone: zone}, Names: []string{testzone}}}
ctx := context.TODO()
for _, tc := range dnsTestCases {
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn(tc.Qname), tc.Qtype)
rec := middleware.NewResponseRecorder(&middleware.TestResponseWriter{})
_, err := fm.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("expected no error, got %v\n", err)
return
}
resp := rec.Msg()
sort.Sort(rrSet(resp.Answer))
sort.Sort(rrSet(resp.Ns))
sort.Sort(rrSet(resp.Extra))
if resp.Rcode != tc.Rcode {
t.Errorf("rcode is %q, expected %q", dns.RcodeToString[resp.Rcode], dns.RcodeToString[tc.Rcode])
t.Logf("%v\n", resp)
continue
}
if len(resp.Answer) != len(tc.Answer) {
t.Errorf("answer for %q contained %d results, %d expected", tc.Qname, len(resp.Answer), len(tc.Answer))
t.Logf("%v\n", resp)
continue
}
if len(resp.Ns) != len(tc.Ns) {
t.Errorf("authority for %q contained %d results, %d expected", tc.Qname, len(resp.Ns), len(tc.Ns))
t.Logf("%v\n", resp)
continue
}
if len(resp.Extra) != len(tc.Extra) {
t.Errorf("additional for %q contained %d results, %d expected", tc.Qname, len(resp.Extra), len(tc.Extra))
t.Logf("%v\n", resp)
continue
}
}
}
type dnsTestCase struct {
Qname string
Qtype uint16
Rcode int
Answer []dns.RR
Ns []dns.RR
Extra []dns.RR
}
func newA(rr string) *dns.A { r, _ := dns.NewRR(rr); return r.(*dns.A) }
func newAAAA(rr string) *dns.AAAA { r, _ := dns.NewRR(rr); return r.(*dns.AAAA) }
func newCNAME(rr string) *dns.CNAME { r, _ := dns.NewRR(rr); return r.(*dns.CNAME) }
func newSRV(rr string) *dns.SRV { r, _ := dns.NewRR(rr); return r.(*dns.SRV) }
func newSOA(rr string) *dns.SOA { r, _ := dns.NewRR(rr); return r.(*dns.SOA) }
func newNS(rr string) *dns.NS { r, _ := dns.NewRR(rr); return r.(*dns.NS) }
func newPTR(rr string) *dns.PTR { r, _ := dns.NewRR(rr); return r.(*dns.PTR) }
func newTXT(rr string) *dns.TXT { r, _ := dns.NewRR(rr); return r.(*dns.TXT) }
func newMX(rr string) *dns.MX { r, _ := dns.NewRR(rr); return r.(*dns.MX) }
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 NS ns-ext.nlnetlabs.nl.
IN NS omval.tednet.nl.
IN NS ext.ns.whyscream.net.
IN MX 1 aspmx.l.google.com.
IN MX 5 alt1.aspmx.l.google.com.
IN MX 5 alt2.aspmx.l.google.com.
IN MX 10 aspmx2.googlemail.com.
IN MX 10 aspmx3.googlemail.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
archive IN CNAME a`
func handler() middleware.Handler {
return middleware.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetRcode(r, dns.RcodeServerFailure)
w.WriteMsg(m)
return dns.RcodeServerFailure, nil
})
}
This diff is collapsed.
package file
import (
"github.com/miekg/coredns/middleware/file/tree"
"github.com/miekg/dns"
)
type Zone struct {
SOA *dns.SOA
SIG []*dns.RRSIG
name string
*tree.Tree
}
func NewZone(name string) *Zone {
return &Zone{name: dns.Fqdn(name), Tree: &tree.Tree{}}
}
func (z *Zone) Insert(r dns.RR) {
z.Tree.Insert(r)
}
func (z *Zone) Delete(r dns.RR) {
z.Tree.Delete(r)
}
package file
import (
"testing"
"github.com/miekg/dns"
)
func TestZoneInsert(t *testing.T) {
z := NewZone("miek.nl")
rr, _ := dns.NewRR("miek.nl. IN A 127.0.0.1")
z.Insert(rr)
t.Logf("%+v\n", z)
elem := z.Get(rr)
t.Logf("%+v\n", elem)
if elem != nil {
t.Logf("%+v\n", elem.Types(dns.TypeA))
}
z.Delete(rr)
t.Logf("%+v\n", z)
elem = z.Get(rr)
t.Logf("%+v\n", elem)
if elem != nil {
t.Logf("%+v\n", elem.Types(dns.TypeA))
}
}
...@@ -179,11 +179,3 @@ func (s State) ErrorMessage(rcode int) *dns.Msg { ...@@ -179,11 +179,3 @@ func (s State) ErrorMessage(rcode int) *dns.Msg {
m.SetRcode(s.Req, rcode) m.SetRcode(s.Req, rcode)
return m return m
} }
// AnswerMessage returns an error message suitable for sending
// back to the client.
func (s State) AnswerMessage() *dns.Msg {
m := new(dns.Msg)
m.SetReply(s.Req)
return m
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment