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
This commit is contained in:
Miek Gieben 2017-10-24 10:16:03 +01:00 committed by GitHub
parent 5f813bcc21
commit fcd0342e42
15 changed files with 269 additions and 120 deletions

View file

@ -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:

View file

@ -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.

View file

@ -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()

View file

@ -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

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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
...
~~~

View file

@ -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

View file

@ -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 {

View file

@ -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 !

View file

@ -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)
}
}
}

View file

@ -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 := ""

145
test/server_reverse_test.go Normal file
View file

@ -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)
}
}
}