Allow overlapping Zones if binding addresses are different (#1530)

* add OverlapChecker, move the test of overlap AFTER the directive setup process, change key of configs to allow multiple same key

* glitch when rebase. init of Config should include the default host

* add tests for the registering of configuration
rename multicast in 'unbound'.
add comments on the validator

* - merged zoneAddr and addrKey that are very similar
- move maps of Validator to zoneAddr, avoinding need to have string representation of zoneaddr
- moving key build for saving Config at Config side instead of dnsContext

* - UT on saving config is now useless.

* - cannot cleanup access to Configs after setup. Deferred function to Start, use it

* - cleanup register unit tests. remove useless function

* - address comments of review. name of validator, comments, simplify registerAndCheck

* - fixes after review. renaming a function and a comment
This commit is contained in:
Francois Tur 2018-02-23 11:54:42 -05:00 committed by Miek Gieben
parent 455040c143
commit 9047bdf3a0
6 changed files with 164 additions and 19 deletions

View file

@ -15,10 +15,17 @@ type zoneAddr struct {
Port string
Transport string // dns, tls or grpc
IPNet *net.IPNet // if reverse zone this hold the IPNet
Address string // used for bound zoneAddr - validation of overlapping
}
// String return the string representation of z.
func (z zoneAddr) String() string { return z.Transport + "://" + z.Zone + ":" + z.Port }
func (z zoneAddr) String() string {
s := z.Transport + "://" + z.Zone + ":" + z.Port
if z.Address != "" {
s += " on " + z.Address
}
return s
}
// Transport returns the protocol of the string s
func Transport(s string) string {
@ -94,3 +101,37 @@ const (
TransportTLS = "tls"
TransportGRPC = "grpc"
)
type zoneOverlap struct {
registeredAddr map[zoneAddr]zoneAddr // each zoneAddr is registered once by its key
unboundOverlap map[zoneAddr]zoneAddr // the "no bind" equiv ZoneAdddr is registered by its original key
}
func newOverlapZone() *zoneOverlap {
return &zoneOverlap{registeredAddr: make(map[zoneAddr]zoneAddr), unboundOverlap: make(map[zoneAddr]zoneAddr)}
}
// registerAndCheck adds a new zoneAddr for validation, it returns information about existing or overlapping with already registered
// we consider that an unbound address is overlapping all bound addresses for same zone, same port
func (zo *zoneOverlap) registerAndCheck(z zoneAddr) (existingZone *zoneAddr, overlappingZone *zoneAddr) {
if exist, ok := zo.registeredAddr[z]; ok {
// exact same zone already registered
return &exist, nil
}
uz := zoneAddr{Zone: z.Zone, Address: "", Port: z.Port, Transport: z.Transport}
if already, ok := zo.unboundOverlap[uz]; ok {
if z.Address == "" {
// current is not bound to an address, but there is already another zone with a bind address registered
return nil, &already
}
if _, ok := zo.registeredAddr[uz]; ok {
// current zone is bound to an address, but there is already an overlapping zone+port with no bind address
return nil, &uz
}
}
// there is no overlap, keep the current zoneAddr for future checks
zo.registeredAddr[z] = z
zo.unboundOverlap[uz] = z
return nil, nil
}

View file

@ -108,3 +108,72 @@ func TestSplitProtocolHostPort(t *testing.T) {
}
}
type checkCall struct {
zone zoneAddr
same bool
overlap bool
overlapKey string
}
type checkTest struct {
sequence []checkCall
}
func TestOverlapAddressChecker(t *testing.T) {
for i, test := range []checkTest{
{sequence: []checkCall{
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, true, false, ""},
},
},
{sequence: []checkCall{
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, true, "dns://.:53"},
},
},
{sequence: []checkCall{
{zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, true, false, ""},
},
},
{sequence: []checkCall{
{zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "54"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "128.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "129.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: ".", Address: "", Port: "53"}, false, true, "dns://.:53 on 129.0.0.1"},
},
},
{sequence: []checkCall{
{zoneAddr{Transport: "dns", Zone: ".", Address: "127.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: "com.", Address: "127.0.0.1", Port: "53"}, false, false, ""},
{zoneAddr{Transport: "dns", Zone: "com.", Address: "", Port: "53"}, false, true, "dns://com.:53 on 127.0.0.1"},
},
},
} {
checker := newOverlapZone()
for _, call := range test.sequence {
same, overlap := checker.registerAndCheck(call.zone)
sZone := call.zone.String()
if (same != nil) != call.same {
t.Errorf("Test %d: error, for zone %s, 'same' (%v) has not the expected value (%v)", i, sZone, same != nil, call.same)
}
if same == nil {
if (overlap != nil) != call.overlap {
t.Errorf("Test %d: error, for zone %s, 'overlap' (%v) has not the expected value (%v)", i, sZone, overlap != nil, call.overlap)
}
if overlap != nil {
if overlap.String() != call.overlapKey {
t.Errorf("Test %d: error, for zone %s, 'overlap Key' (%v) has not the expected value (%v)", i, sZone, overlap.String(), call.overlapKey)
}
}
}
}
}
}

View file

@ -2,6 +2,7 @@ package dnsserver
import (
"crypto/tls"
"fmt"
"net"
"github.com/coredns/coredns/plugin"
@ -68,16 +69,22 @@ func (c *Config) HostAddresses() string {
return all
}
// keyForConfig build a key for identifying the configs during setup time
func keyForConfig(blocIndex int, blocKeyIndex int) string {
return fmt.Sprintf("%d:%d", blocIndex, blocKeyIndex)
}
// GetConfig gets the Config that corresponds to c.
// If none exist nil is returned.
func GetConfig(c *caddy.Controller) *Config {
ctx := c.Context().(*dnsContext)
if cfg, ok := ctx.keysToConfigs[c.Key]; ok {
key := keyForConfig(c.ServerBlockIndex, c.ServerBlockKeyIndex)
if cfg, ok := ctx.keysToConfigs[key]; ok {
return cfg
}
// we should only get here during tests because directive
// actions typically skip the server blocks where we make
// the configs.
ctx.saveConfig(c.Key, &Config{ListenHosts: []string{""}})
ctx.saveConfig(key, &Config{ListenHosts: []string{""}})
return GetConfig(c)
}

View file

@ -55,28 +55,23 @@ func (h *dnsContext) saveConfig(key string, cfg *Config) {
// be parsed and executed.
func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddyfile.ServerBlock) ([]caddyfile.ServerBlock, error) {
// Normalize and check all the zone names and check for duplicates
dups := map[string]string{}
for _, s := range serverBlocks {
for i, k := range s.Keys {
for ib, s := range serverBlocks {
for ik, k := range s.Keys {
za, err := normalizeZone(k)
if err != nil {
return nil, err
}
s.Keys[i] = za.String()
if v, ok := dups[za.String()]; ok {
return nil, fmt.Errorf("cannot serve %s - zone already defined for %v", za, v)
}
dups[za.String()] = za.String()
s.Keys[ik] = za.String()
// Save the config to our master list, and key it for lookups.
cfg := &Config{
Zone: za.Zone,
ListenHosts: []string{""},
Port: za.Port,
Transport: za.Transport,
ListenHosts: []string{""},
}
keyConfig := keyForConfig(ib, ik)
if za.IPNet == nil {
h.saveConfig(za.String(), cfg)
h.saveConfig(keyConfig, cfg)
continue
}
@ -91,7 +86,7 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
return za.IPNet.Contains(net.ParseIP(addr))
}
}
h.saveConfig(za.String(), cfg)
h.saveConfig(keyConfig, cfg)
}
}
return serverBlocks, nil
@ -100,6 +95,13 @@ func (h *dnsContext) InspectServerBlocks(sourceFile string, serverBlocks []caddy
// MakeServers uses the newly-created siteConfigs to create and return a list of server instances.
func (h *dnsContext) MakeServers() ([]caddy.Server, error) {
// Now that all Keys and Directives are parsed and initialized
// lets verify that there is no overlap on the zones and addresses to listen for
errValid := h.validateZonesAndListeningAddresses()
if errValid != nil {
return nil, errValid
}
// we must map (group) each config to a bind address
groups, err := groupConfigsByListenAddr(h.configs)
if err != nil {
@ -183,14 +185,35 @@ func (c *Config) Handlers() []plugin.Handler {
return hs
}
func (h *dnsContext) validateZonesAndListeningAddresses() error {
//Validate Zone and addresses
checker := newOverlapZone()
for _, conf := range h.configs {
for _, h := range conf.ListenHosts {
// Validate the overlapping of ZoneAddr
akey := zoneAddr{Transport: conf.Transport, Zone: conf.Zone, Address: h, Port: conf.Port}
existZone, overlapZone := checker.registerAndCheck(akey)
if existZone != nil {
return fmt.Errorf("cannot serve %s - it is already defined", akey.String())
}
if overlapZone != nil {
return fmt.Errorf("cannot serve %s - zone overlap listener capacity with %v", akey.String(), overlapZone.String())
}
}
}
return nil
}
// groupSiteConfigsByListenAddr groups site configs by their listen
// (bind) address, so sites that use the same listener can be served
// on the same server instance. The return value maps the listen
// address (what you pass into net.Listen) to the list of site configs.
// This function does NOT vet the configs to ensure they are compatible.
func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
groups := make(map[string][]*Config)
groups := make(map[string][]*Config)
for _, conf := range configs {
for _, h := range conf.ListenHosts {
addr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(h, conf.Port))
@ -201,6 +224,7 @@ func groupConfigsByListenAddr(configs []*Config) (map[string][]*Config, error) {
groups[addrstr] = append(groups[addrstr], conf)
}
}
return groups, nil
}

View file

@ -51,7 +51,7 @@ func TestGroupingServers(t *testing.T) {
expectedGroups: []string{"dns://:53", "dns://:54"},
failing: false},
// 2 configs on same port, same broadcast address, diff zones -> 1 group
// 2 configs on same port, both not using bind, diff zones -> 1 group
{configs: []*Config{
{Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{""}},
{Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{""}},
@ -59,7 +59,7 @@ func TestGroupingServers(t *testing.T) {
expectedGroups: []string{"dns://:53"},
failing: false},
// 2 configs on same port, same address, diff zones -> 1 group
// 2 configs on same port, one addressed - one not using bind, diff zones -> 1 group
{configs: []*Config{
{Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1"}},
{Transport: "dns", Zone: ".", Port: "54", ListenHosts: []string{""}},
@ -74,7 +74,7 @@ func TestGroupingServers(t *testing.T) {
expectedGroups: []string{"dns://127.0.0.1:53", "dns://[::1]:53", "dns://:54"},
failing: false},
// 2 configs on same port, same unicast address, diff zones -> 1 group
// 2 configs on same port, same address, diff zones -> 1 group
{configs: []*Config{
{Transport: "dns", Zone: ".", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}},
{Transport: "dns", Zone: "com.", Port: "53", ListenHosts: []string{"127.0.0.1", "::1"}},

View file

@ -65,6 +65,10 @@ func NewServer(addr string, group []*Config) (*Server, error) {
// set the config per zone
s.zones[site.Zone] = site
// compile custom plugin for everything
if site.registry != nil {
// this config is already computed with the chain of plugin
continue
}
var stack plugin.Handler
for i := len(site.Plugin) - 1; i >= 0; i-- {
stack = site.Plugin[i](stack)