From fcd0342e42a8be5a1cfe41304f0a8099b1bc0e06 Mon Sep 17 00:00:00 2001 From: Miek Gieben Date: Tue, 24 Oct 2017 10:16:03 +0100 Subject: [PATCH] CIDR query routing (#1159) * core: allow all CIDR ranges in zone specifications Allow (e.g.) a v4 reverse on a /17. If a zone is specified in such a way a FilterFunc is set in the config. This filter is checked against incoming queries. For all other queries this adds a 'x != nil' check which will not impact performace too much. Benchmark function is added as well to check for this as wel. Add multiple tests in tests/server_reverse_test.go. Benchmark shows in the non-reverse case this hardly impact the speed: ~~~ classless: pkg: github.com/coredns/coredns/core/dnsserver BenchmarkCoreServeDNS-4 1000000 1431 ns/op 16 B/op 1 allocs/op pkg: github.com/coredns/coredns/core/dnsserver BenchmarkCoreServeDNS-4 1000000 1429 ns/op 16 B/op 1 allocs/op master: pkg: github.com/coredns/coredns/core/dnsserver BenchmarkCoreServeDNS-4 1000000 1412 ns/op 16 B/op 1 allocs/op pkg: github.com/coredns/coredns/core/dnsserver BenchmarkCoreServeDNS-4 1000000 1429 ns/op 16 B/op 1 allocs/op ~~~ * README.md updates --- README.md | 8 +- core/dnsserver/address.go | 8 +- core/dnsserver/address_test.go | 5 +- core/dnsserver/config.go | 6 + core/dnsserver/directives.go | 64 ----------- core/dnsserver/register.go | 20 +++- core/dnsserver/server.go | 25 ++-- core/dnsserver/server_test.go | 20 ++-- plugin/etcd/README.md | 5 +- plugin/kubernetes/README.md | 9 +- plugin/kubernetes/setup_reverse_test.go | 2 +- plugin/normalize.go | 40 +++---- plugin/normalize_test.go | 30 ++++- plugin/pkg/dnsutil/reverse.go | 2 +- test/server_reverse_test.go | 145 ++++++++++++++++++++++++ 15 files changed, 269 insertions(+), 120 deletions(-) delete mode 100644 core/dnsserver/directives.go create mode 100644 test/server_reverse_test.go diff --git a/README.md b/README.md index 9f001857c..bd8afa8fb 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,11 @@ IP addresses are also allowed. They are automatically converted to reverse zones ~~~ Means you are authoritative for `0.0.10.in-addr.arpa.`. -The netmask must be dividable by 8, if it is not the reverse conversion is not done. This also works -for IPv6 addresses. If for some reason you want to serve a zone named `10.0.0.0/24` add the closing -dot: `10.0.0.0/24.` as this also stops the conversion. +This also works for IPv6 addresses. If for some reason you want to serve a zone named `10.0.0.0/24` +add the closing dot: `10.0.0.0/24.` as this also stops the conversion. + +This even works for CIDR (See RFC 1518 and 1519) addressing, i.e `10.0.0.0/25`, CoreDNS will then +check if the `in-addr` request falls in the correct range. Listening on TLS and for gRPC? Use: diff --git a/core/dnsserver/address.go b/core/dnsserver/address.go index e8ef13dbf..e17c9c706 100644 --- a/core/dnsserver/address.go +++ b/core/dnsserver/address.go @@ -1,6 +1,7 @@ package dnsserver import ( + "net" "strings" "github.com/coredns/coredns/plugin" @@ -11,7 +12,8 @@ import ( type zoneAddr struct { Zone string Port string - Transport string // dns, tls or grpc + Transport string // dns, tls or grpc + IPNet *net.IPNet // if reverse zone this hold the IPNet } // String return the string representation of z. @@ -50,7 +52,7 @@ func normalizeZone(str string) (zoneAddr, error) { str = str[len(TransportGRPC+"://"):] } - host, port, err := plugin.SplitHostPort(str) + host, port, ipnet, err := plugin.SplitHostPort(str) if err != nil { return zoneAddr{}, err } @@ -67,7 +69,7 @@ func normalizeZone(str string) (zoneAddr, error) { } } - return zoneAddr{Zone: dns.Fqdn(host), Port: port, Transport: trans}, nil + return zoneAddr{Zone: dns.Fqdn(host), Port: port, Transport: trans, IPNet: ipnet}, nil } // Supported transports. diff --git a/core/dnsserver/address_test.go b/core/dnsserver/address_test.go index 5bf0d4f3d..a2785f6aa 100644 --- a/core/dnsserver/address_test.go +++ b/core/dnsserver/address_test.go @@ -45,8 +45,9 @@ func TestNormalizeZoneReverse(t *testing.T) { {"10.0.0.0/24.:53", "dns://10.0.0.0/24.:53", false}, // non %8==0 netmasks - {"2003::53/67", "dns://2003::53/67.:53", false}, - {"10.0.0.0/25.", "dns://10.0.0.0/25.:53", false}, + {"2003::53/67", "dns://0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.3.0.0.2.ip6.arpa.:53", false}, + {"10.0.0.0/25.", "dns://10.0.0.0/25.:53", false}, // has dot + {"10.0.0.0/25", "dns://0.0.10.in-addr.arpa.:53", false}, } { addr, err := normalizeZone(test.input) actual := addr.String() diff --git a/core/dnsserver/config.go b/core/dnsserver/config.go index 0d25183c0..0f75a7f86 100644 --- a/core/dnsserver/config.go +++ b/core/dnsserver/config.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "github.com/coredns/coredns/plugin" + "github.com/mholt/caddy" ) @@ -29,6 +30,11 @@ type Config struct { // DNS-over-TLS or DNS-over-gRPC. Transport string + // If this function is not nil it will be used to further filter access + // to this handler. The primary use is to limit access to a reverse zone + // on a non-octet boundary, i.e. /17 + FilterFunc func(string) bool + // TLSConfig when listening for encrypted connections (gRPC, DNS-over-TLS). TLSConfig *tls.Config diff --git a/core/dnsserver/directives.go b/core/dnsserver/directives.go deleted file mode 100644 index bc37baf11..000000000 --- a/core/dnsserver/directives.go +++ /dev/null @@ -1,64 +0,0 @@ -package dnsserver - -import ( - "fmt" - "os" - "strings" -) - -// RegisterDevDirective splices name into the list of directives -// immediately before another directive. This function is ONLY -// for plugin development purposes! NEVER use it for a plugin -// that you are not currently building. If before is empty, -// the directive will be appended to the end of the list. -// -// It is imperative that directives execute in the proper -// order, and hard-coding the list of directives guarantees -// a correct, absolute order every time. This function is -// convenient when developing a plugin, but it does not -// guarantee absolute ordering. Multiple plugins registering -// directives with this function will lead to non- -// deterministic builds and buggy software. -// -// Directive names must be lower-cased and unique. Any errors -// here are fatal, and even successful calls print a message -// to stdout as a reminder to use it only in development. -func RegisterDevDirective(name, before string) { - if name == "" { - fmt.Println("[FATAL] Cannot register empty directive name") - os.Exit(1) - } - if strings.ToLower(name) != name { - fmt.Printf("[FATAL] %s: directive name must be lowercase\n", name) - os.Exit(1) - } - for _, dir := range directives { - if dir == name { - fmt.Printf("[FATAL] %s: directive name already exists\n", name) - os.Exit(1) - } - } - if before == "" { - directives = append(directives, name) - } else { - var found bool - for i, dir := range directives { - if dir == before { - directives = append(directives[:i], append([]string{name}, directives[i:]...)...) - found = true - break - } - } - if !found { - fmt.Printf("[FATAL] %s: directive not found\n", before) - os.Exit(1) - } - } - msg := fmt.Sprintf("Registered directive '%s' ", name) - if before == "" { - msg += "at end of list" - } else { - msg += fmt.Sprintf("before '%s'", before) - } - fmt.Printf("[INFO] %s\n", msg) -} diff --git a/core/dnsserver/register.go b/core/dnsserver/register.go index d10227a65..ebe4fc112 100644 --- a/core/dnsserver/register.go +++ b/core/dnsserver/register.go @@ -4,9 +4,11 @@ import ( "flag" "fmt" "net" + "strings" "time" "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnsutil" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyfile" @@ -66,12 +68,28 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy } dups[za.String()] = za.String() - // Save the config to our master list, and key it for lookups + // Save the config to our master list, and key it for lookups. cfg := &Config{ Zone: za.Zone, Port: za.Port, Transport: za.Transport, } + if za.IPNet == nil { + h.saveConfig(za.String(), cfg) + continue + } + + ones, bits := za.IPNet.Mask.Size() + if (bits-ones)%8 != 0 { // only do this for non-octet bounderies + cfg.FilterFunc = func(s string) bool { + // TODO(miek): strings.ToLower! Slow and allocates new string. + addr := dnsutil.ExtractAddressFromReverse(strings.ToLower(s)) + if addr == "" { + return true + } + return za.IPNet.Contains(net.ParseIP(addr)) + } + } h.saveConfig(za.String(), cfg) } } diff --git a/core/dnsserver/server.go b/core/dnsserver/server.go index bb9a87a12..38d8600c7 100644 --- a/core/dnsserver/server.go +++ b/core/dnsserver/server.go @@ -40,7 +40,7 @@ type Server struct { classChaos bool // allow non-INET class queries } -// NewServer returns a new CoreDNS server and compiles all plugin in to it. By default CH class +// NewServer returns a new CoreDNS server and compiles all plugins in to it. By default CH class // queries are blocked unless the chaos or proxy is loaded. func NewServer(addr string, group []*Config) (*Server, error) { @@ -225,11 +225,22 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) if h, ok := s.zones[string(b[:l])]; ok { if r.Question[0].Qtype != dns.TypeDS { - rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) - if !plugin.ClientWrite(rcode) { - DefaultErrorFunc(w, r, rcode) + if h.FilterFunc == nil { + rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) + if !plugin.ClientWrite(rcode) { + DefaultErrorFunc(w, r, rcode) + } + return + } + // FilterFunc is set, call it to see if we should use this handler. + // This is given to full query name. + if h.FilterFunc(q) { + rcode, _ := h.pluginChain.ServeDNS(ctx, w, r) + if !plugin.ClientWrite(rcode) { + DefaultErrorFunc(w, r, rcode) + } + return } - return } // The type is DS, keep the handler, but keep on searching as maybe we are serving // the parent as well and the DS should be routed to it - this will probably *misroute* DS @@ -244,8 +255,8 @@ func (s *Server) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) } } - if dshandler != nil { - // DS request, and we found a zone, use the handler for the query + if r.Question[0].Qtype == dns.TypeDS && dshandler != nil { + // DS request, and we found a zone, use the handler for the query. rcode, _ := dshandler.pluginChain.ServeDNS(ctx, w, r) if !plugin.ClientWrite(rcode) { DefaultErrorFunc(w, r, rcode) diff --git a/core/dnsserver/server_test.go b/core/dnsserver/server_test.go index 16235fd7c..75f5b98f7 100644 --- a/core/dnsserver/server_test.go +++ b/core/dnsserver/server_test.go @@ -18,7 +18,7 @@ func (tp testPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns. func (tp testPlugin) Name() string { return "testplugin" } -func testConfig(transport string) *Config { +func testConfig(transport string, p plugin.Handler) *Config { c := &Config{ Zone: "example.com.", Transport: transport, @@ -27,31 +27,31 @@ func testConfig(transport string) *Config { Debug: false, } - c.AddPlugin(func(next plugin.Handler) plugin.Handler { return testPlugin{} }) + c.AddPlugin(func(next plugin.Handler) plugin.Handler { return p }) return c } func TestNewServer(t *testing.T) { - _, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns")}) + _, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) if err != nil { - t.Errorf("Expected no error for NewServer, got %s.", err) + t.Errorf("Expected no error for NewServer, got %s", err) } - _, err = NewServergRPC("127.0.0.1:53", []*Config{testConfig("grpc")}) + _, err = NewServergRPC("127.0.0.1:53", []*Config{testConfig("grpc", testPlugin{})}) if err != nil { - t.Errorf("Expected no error for NewServergRPC, got %s.", err) + t.Errorf("Expected no error for NewServergRPC, got %s", err) } - _, err = NewServerTLS("127.0.0.1:53", []*Config{testConfig("tls")}) + _, err = NewServerTLS("127.0.0.1:53", []*Config{testConfig("tls", testPlugin{})}) if err != nil { - t.Errorf("Expected no error for NewServerTLS, got %s.", err) + t.Errorf("Expected no error for NewServerTLS, got %s", err) } } func BenchmarkCoreServeDNS(b *testing.B) { - s, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns")}) + s, err := NewServer("127.0.0.1:53", []*Config{testConfig("dns", testPlugin{})}) if err != nil { - b.Errorf("Expected no error for NewServer, got %s.", err) + b.Errorf("Expected no error for NewServer, got %s", err) } ctx := context.TODO() diff --git a/plugin/etcd/README.md b/plugin/etcd/README.md index 8b8680f25..f8e3353ca 100644 --- a/plugin/etcd/README.md +++ b/plugin/etcd/README.md @@ -84,11 +84,10 @@ when resolving external pointing CNAMEs. Reverse zones are supported. You need to make CoreDNS aware of the fact that you are also authoritative for the reverse. For instance if you want to add the reverse for 10.0.0.0/24, you'll -need to add the zone `0.0.10.in-addr.arpa` to the list of zones. (The fun starts with IPv6 reverse zones -in the ip6.arpa domain.) Showing a snippet of a Corefile: +need to add the zone `0.0.10.in-addr.arpa` to the list of zones. Showing a snippet of a Corefile: ~~~ -etcd skydns.local 0.0.10.in-addr.arpa { +etcd skydns.local 10.0.0.0/24 { stubzones ... ~~~ diff --git a/plugin/kubernetes/README.md b/plugin/kubernetes/README.md index f7887f74d..42fc54543 100644 --- a/plugin/kubernetes/README.md +++ b/plugin/kubernetes/README.md @@ -76,13 +76,12 @@ kubernetes [ZONES...] { ## Examples -Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster. -Also handle all `PTR` requests for `10.0.0.0/16` . Verify the existence of pods when answering pod -requests. Resolve upstream records against `10.102.3.10`. Note we show the entire server block -here: +Handle all queries in the `cluster.local` zone. Connect to Kubernetes in-cluster. Also handle all +`in-addr.arpa` `PTR` requests for `10.0.0.0/17` . Verify the existence of pods when answering pod +requests. Resolve upstream records against `10.102.3.10`. Note we show the entire server block here: ~~~ txt -10.0.0.0/16 cluster.local { +10.0.0.0/17 cluster.local { kubernetes { pods verified upstream 10.102.3.10:53 diff --git a/plugin/kubernetes/setup_reverse_test.go b/plugin/kubernetes/setup_reverse_test.go index ed51a7410..626874e6d 100644 --- a/plugin/kubernetes/setup_reverse_test.go +++ b/plugin/kubernetes/setup_reverse_test.go @@ -12,7 +12,7 @@ func TestKubernetesParseReverseZone(t *testing.T) { expectedZones []string // expected count of defined zones. }{ {`kubernetes coredns.local 10.0.0.0/16`, []string{"coredns.local.", "0.10.in-addr.arpa."}}, - {`kubernetes coredns.local 10.0.0.0/17`, []string{"coredns.local.", "10.0.0.0/17."}}, + {`kubernetes coredns.local 10.0.0.0/17`, []string{"coredns.local.", "0.10.in-addr.arpa."}}, } for i, tc := range tests { diff --git a/plugin/normalize.go b/plugin/normalize.go index 75b9c53c8..f8cbcf7a0 100644 --- a/plugin/normalize.go +++ b/plugin/normalize.go @@ -75,13 +75,14 @@ func (h Host) Normalize() string { // The error can be ignore here, because this function is called after the corefile // has already been vetted. - host, _, _ := SplitHostPort(s) + host, _, _, _ := SplitHostPort(s) return Name(host).Normalize() } // SplitHostPort splits s up in a host and port portion, taking reverse address notation into account. -// String the string s should *not* be prefixed with any protocols, i.e. dns:// -func SplitHostPort(s string) (host, port string, err error) { +// String the string s should *not* be prefixed with any protocols, i.e. dns://. The returned ipnet is the +// *net.IPNet that is used when the zone is a reverse and a netmask is given. +func SplitHostPort(s string) (host, port string, ipnet *net.IPNet, err error) { // If there is: :[0-9]+ on the end we assume this is the port. This works for (ascii) domain // names and our reverse syntax, which always needs a /mask *before* the port. // So from the back, find first colon, and then check if its a number. @@ -89,7 +90,7 @@ func SplitHostPort(s string) (host, port string, err error) { colon := strings.LastIndex(s, ":") if colon == len(s)-1 { - return "", "", fmt.Errorf("expecting data after last colon: %q", s) + return "", "", nil, fmt.Errorf("expecting data after last colon: %q", s) } if colon != -1 { if p, err := strconv.Atoi(s[colon+1:]); err == nil { @@ -100,33 +101,34 @@ func SplitHostPort(s string) (host, port string, err error) { // TODO(miek): this should take escaping into account. if len(host) > 255 { - return "", "", fmt.Errorf("specified zone is too long: %d > 255", len(host)) + return "", "", nil, fmt.Errorf("specified zone is too long: %d > 255", len(host)) } _, d := dns.IsDomainName(host) if !d { - return "", "", fmt.Errorf("zone is not a valid domain name: %s", host) + return "", "", nil, fmt.Errorf("zone is not a valid domain name: %s", host) } - // Check if it parses as a reverse zone, if so we use that. Must be fully - // specified IP and mask and mask % 8 = 0. - ip, net, err := net.ParseCIDR(host) + // Check if it parses as a reverse zone, if so we use that. Must be fully specified IP and mask. + ip, n, err := net.ParseCIDR(host) + ones, bits := 0, 0 if err == nil { if rev, e := dns.ReverseAddr(ip.String()); e == nil { - ones, bits := net.Mask.Size() - if (bits-ones)%8 == 0 { - offset, end := 0, false - for i := 0; i < (bits-ones)/8; i++ { - offset, end = dns.NextLabel(rev, offset) - if end { - break - } + ones, bits = n.Mask.Size() + // Get the first lower octet boundary to see what encompassing zone we should be authoritative for. + mod := (bits - ones) % 8 + nearest := (bits - ones) + mod + offset, end := 0, false + for i := 0; i < nearest/8; i++ { + offset, end = dns.NextLabel(rev, offset) + if end { + break } - host = rev[offset:] } + host = rev[offset:] } } - return host, port, nil + return host, port, n, nil } // Duplicated from core/dnsserver/address.go ! diff --git a/plugin/normalize_test.go b/plugin/normalize_test.go index 3eb9c5231..e81d32629 100644 --- a/plugin/normalize_test.go +++ b/plugin/normalize_test.go @@ -70,7 +70,7 @@ func TestNameNormalize(t *testing.T) { func TestHostNormalize(t *testing.T) { hosts := []string{".:53", ".", "example.org:53", "example.org.", "example.org.:53", "example.org.", - "10.0.0.0/8:53", "10.in-addr.arpa.", "10.0.0.0/9", "10.0.0.0/9.", + "10.0.0.0/8:53", "10.in-addr.arpa.", "10.0.0.0/9", "10.in-addr.arpa.", "dns://example.org", "example.org."} for i := 0; i < len(hosts); i += 2 { @@ -82,3 +82,31 @@ func TestHostNormalize(t *testing.T) { } } } + +func TestSplitHostPortReverse(t *testing.T) { + tests := map[string]int{ + "example.org.": 0, + "10.0.0.0/9": 32 - 9, + "10.0.0.0/8": 32 - 8, + "10.0.0.0/17": 32 - 17, + "10.0.0.0/0": 32 - 0, + "10.0.0.0/64": 0, + "10.0.0.0": 0, + "10.0.0": 0, + "2003::1/65": 128 - 65, + } + for in, expect := range tests { + _, _, n, err := SplitHostPort(in) + if err != nil { + t.Errorf("Expected no error, got %q for %s", in, err) + } + if n == nil { + continue + } + ones, bits := n.Mask.Size() + got := bits - ones + if got != expect { + t.Errorf("Expected %d, got %d for %s", expect, got, in) + } + } +} diff --git a/plugin/pkg/dnsutil/reverse.go b/plugin/pkg/dnsutil/reverse.go index daf9cc600..f92cf18b5 100644 --- a/plugin/pkg/dnsutil/reverse.go +++ b/plugin/pkg/dnsutil/reverse.go @@ -9,7 +9,7 @@ import ( // into an IP address. This works for ipv4 or ipv6. // // 54.119.58.176.in-addr.arpa. becomes 176.58.119.54. If the conversion -// failes the empty string is returned. +// fails the empty string is returned. func ExtractAddressFromReverse(reverseName string) string { search := "" diff --git a/test/server_reverse_test.go b/test/server_reverse_test.go new file mode 100644 index 000000000..6434d867d --- /dev/null +++ b/test/server_reverse_test.go @@ -0,0 +1,145 @@ +package test + +import ( + "strings" + "testing" + + "github.com/miekg/dns" +) + +func TestClasslessReverse(t *testing.T) { + // 25 -> so anything above 1.127 won't be answered, below is OK. + corefile := `192.168.1.0/25:0 { + whoami +} +` + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, // in range + {"192.168.1.1", dns.RcodeSuccess}, // in range + {"192.168.1.127", dns.RcodeSuccess}, // in range + + {"192.168.1.128", dns.RcodeRefused}, // out of range + {"192.168.1.129", dns.RcodeRefused}, // out of range + {"192.168.1.255", dns.RcodeRefused}, // out of range + {"192.168.2.0", dns.RcodeRefused}, // different zone + } + + m := new(dns.Msg) + for i, tc := range tests { + inaddr, _ := dns.ReverseAddr(tc.addr) + m.SetQuestion(inaddr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +} + +func TestReverse(t *testing.T) { + corefile := `192.168.1.0/24:0 { + whoami +} +` + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, + {"192.168.1.1", dns.RcodeSuccess}, + {"192.168.1.127", dns.RcodeSuccess}, + {"192.168.1.128", dns.RcodeSuccess}, + {"1.168.192.in-addr.arpa.", dns.RcodeSuccess}, + + {"2.168.192.in-addr.arpa.", dns.RcodeRefused}, + } + + m := new(dns.Msg) + for i, tc := range tests { + inaddr := tc.addr + var err error + if !strings.HasSuffix(tc.addr, ".arpa.") { + inaddr, err = dns.ReverseAddr(tc.addr) + if err != nil { + t.Fatalf("Test %d, failed to convert %s", i, tc.addr) + } + tc.addr = inaddr + } + + m.SetQuestion(tc.addr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +} + +func TestReverseInAddr(t *testing.T) { + corefile := `1.168.192.in-addr.arpa:0 { + whoami +} +` + s, udp, _, err := CoreDNSServerAndPorts(corefile) + if err != nil { + t.Fatalf("Could not get CoreDNS serving instance: %s", err) + } + defer s.Stop() + + tests := []struct { + addr string + rcode int + }{ + {"192.168.1.0", dns.RcodeSuccess}, + {"192.168.1.1", dns.RcodeSuccess}, + {"192.168.1.127", dns.RcodeSuccess}, + {"192.168.1.128", dns.RcodeSuccess}, + {"1.168.192.in-addr.arpa.", dns.RcodeSuccess}, + + {"2.168.192.in-addr.arpa.", dns.RcodeRefused}, + } + + m := new(dns.Msg) + for i, tc := range tests { + inaddr := tc.addr + var err error + if !strings.HasSuffix(tc.addr, ".arpa.") { + inaddr, err = dns.ReverseAddr(tc.addr) + if err != nil { + t.Fatalf("Test %d, failed to convert %s", i, tc.addr) + } + tc.addr = inaddr + } + + m.SetQuestion(tc.addr, dns.TypeA) + + r, e := dns.Exchange(m, udp) + if e != nil { + t.Errorf("Test %d, expected no error, got %q", i, e) + } + if r.Rcode != tc.rcode { + t.Errorf("Test %d, expected %d, got %d for %s", i, tc.rcode, r.Rcode, tc.addr) + } + } +}