Skip to content

Commit

Permalink
Merge pull request #441 from vivint-smarthome/master
Browse files Browse the repository at this point in the history
Add support for zone-specific resolvers
  • Loading branch information
sargun committed Sep 27, 2016
2 parents a2239fc + df88e88 commit fb1dcdd
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 18 deletions.
7 changes: 6 additions & 1 deletion docs/docs/configuration-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ The configuration file should include the following fields:
"domain": "mesos",
"port": 53,
"resolvers": ["169.254.169.254"],
"zoneResolvers": {
"weave": ["172.17.0.1"]
},
"timeout": 5,
"httpon": true,
"dnson": true,
Expand Down Expand Up @@ -71,7 +74,9 @@ It is sufficient to specify just one of the `zk` or `masters` field. If both are
`port` is the port number that Mesos-DNS monitors for incoming DNS requests. Requests can be sent over TCP or UDP. We recommend you use port `53` as several applications assume that the DNS server listens to this port. The default value is `53`.

`resolvers` is a comma separated list with the IP addresses of external DNS servers that Mesos-DNS will contact to resolve any DNS requests outside the `domain`. We ***recommend*** that you list the nameservers specified in the `/etc/resolv.conf` on the server Mesos-DNS is running. Alternatively, you can list `8.8.8.8`, which is the [Google public DNS](https://developers.google.com/speed/public-dns/) address. The `resolvers` field is required.


`zoneResolvers` is a dictionary of zone-specific external DNS servers, where the key is the matching zone (sans leading / trailing .). You can use this configuration option to route a subset of DNS queries to a specific set of DNS servers. Note, general, catch-all resolvers are still specified with `resolvers`.

`timeout` is the timeout threshold, in seconds, for connections and requests to external DNS requests. The default value is 5 seconds.

`listener` is the IP address of Mesos-DNS. In SOA replies, Mesos-DNS identifies hostname `mesos-dns.domain` as the primary nameserver for the domain. It uses this IP address in an A record for `mesos-dns.domain`. The default value is "0.0.0.0", which instructs Mesos-DNS to create an A record for every IP address associated with a network interface on the server that runs the Mesos-DNS process.
Expand Down
17 changes: 17 additions & 0 deletions records/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Config struct {
SOARname string // email of admin esponsible
// Mesos master(s): a list of IP:port pairs for one or more Mesos masters
Masters []string
// DNS server: IP address of the DNS server for forwarded accesses
ZoneResolvers map[string][]string
// DNS server: a list of IP addresses or IP:port pairs for DNS servers for forwarded accesses
Resolvers []string
// IPSources is the prioritized list of task IP sources
Expand Down Expand Up @@ -105,6 +107,7 @@ func NewConfig() Config {
SOARetry: 600,
SOAExpire: 86400,
SOAMinttl: 60,
ZoneResolvers: map[string][]string{},
Resolvers: []string{"8.8.8.8"},
Listener: "0.0.0.0",
HTTPListener: "0.0.0.0",
Expand Down Expand Up @@ -147,6 +150,11 @@ func SetConfig(cjson string) Config {

c.Domain = strings.ToLower(c.Domain)

err = validateDomainName(c.Domain)
if err != nil {
logging.Error.Fatalf("%s is not a valid domain name", c.Domain)
}

c.initSOA()

if c.CACertFile != "" {
Expand Down Expand Up @@ -190,6 +198,9 @@ func (c *Config) initResolvers() {
if err := validateResolvers(c.Resolvers); err != nil {
logging.Error.Fatal(err)
}
if err := validateZoneResolvers(c.ZoneResolvers, c.Domain); err != nil {
logging.Error.Fatal(err)
}
}
}

Expand All @@ -202,6 +213,10 @@ func (c *Config) initSOA() {

func (c Config) log() {
// print configuration file
zoneResolversJSON, err := json.Marshal(c.ZoneResolvers)
if err != nil {
zoneResolversJSON = []byte(fmt.Sprintf("error: %v", err))
}
logging.Verbose.Println("Mesos-DNS configuration:")
logging.Verbose.Println(" - Masters: " + strings.Join(c.Masters, ", "))
logging.Verbose.Println(" - Zookeeper: ", c.Zk)
Expand All @@ -215,6 +230,8 @@ func (c Config) log() {
logging.Verbose.Println(" - TTL: ", c.TTL)
logging.Verbose.Println(" - Timeout: ", c.Timeout)
logging.Verbose.Println(" - StateTimeoutSeconds: ", c.StateTimeoutSeconds)

logging.Verbose.Println(" - ZoneResolvers: " + string(zoneResolversJSON))
logging.Verbose.Println(" - Resolvers: " + strings.Join(c.Resolvers, ", "))
logging.Verbose.Println(" - ExternalOn: ", c.ExternalOn)
logging.Verbose.Println(" - SOAMname: " + c.SOAMname)
Expand Down
48 changes: 48 additions & 0 deletions records/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ package records
import (
"fmt"
"net"
"regexp"
"strconv"
"strings"
)

var dnsValidationRegex = regexp.MustCompile(`^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$`)

func validateEnabledServices(c *Config) error {
if !c.DNSOn && !c.HTTPOn {
return fmt.Errorf("Either DNS or HTTP server should be on")
Expand Down Expand Up @@ -51,6 +55,50 @@ func validateResolvers(rs []string) error {
return nil
}

func validateDomainName(domain string) error {
if !dnsValidationRegex.MatchString(domain) {
return fmt.Errorf("Invalid domain name: %s", domain)
}
return nil
}

func validateZoneResolvers(zrs map[string][]string, mesosDomain string) (
err error) {

allDomains := make([]string, 0, len(zrs)+1)

for domain, rs := range zrs {
if len(rs) == 0 {
return fmt.Errorf("ZoneResolver %v is empty", domain)
}
err = validateDomainName(domain)
if err != nil {
return err
}

err = validateResolvers(rs)
if err != nil {
return
}
if domain == mesosDomain {
return fmt.Errorf("Can't specify ZoneResolver for Mesos domain (%v)",
mesosDomain)
}
allDomains = append(allDomains, "."+domain)
}
allDomains = append(allDomains, "."+mesosDomain)
for _, a := range allDomains {
for _, b := range allDomains {
if (a != b) &&
strings.HasSuffix(a, b) {
return fmt.Errorf("Ambiguous zone resolvers: %v is masked by %v",
a, b)
}
}
}
return
}

func normalizeResolver(hostPort string) (string, error) {
host, port, err := net.SplitHostPort(hostPort)
if err != nil {
Expand Down
61 changes: 61 additions & 0 deletions records/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ import (
"testing"
)

func TestValidateDomain(t *testing.T) {
testDomain := func(domain string, shouldSucceed bool) {
err := validateDomainName(domain)
if shouldSucceed && err != nil {
t.Errorf("validation should have succeeded for %s", domain)
}
if !shouldSucceed && err == nil {
t.Errorf("validation should have failed for %s", domain)
}
}

testDomain("", false)
testDomain("1-awesome.domain.e", true)
testDomain(".invalid.com", false)
testDomain("invalid.com.", false)
testDomain("single-name", true)
testDomain("name", true)
}

func TestValidateMasters(t *testing.T) {
for i, tc := range []validationTest{
{nil, true},
Expand Down Expand Up @@ -74,3 +93,45 @@ func validate(t *testing.T, i int, tc validationTest, f func([]string) error) {
t.Fatalf("test %d failed, expected validation error for resolvers(%d) %v", i, len(tc.in), tc.in)
}
}

func TestValidateZoneResolvers(t *testing.T) {
ips := []string{"8.8.8.8"}

fn := func(zrs map[string][]string) error {
return validateZoneResolvers(zrs, "dc.mesos")
}

for i, tc := range []zoneValidationTest{
{nil, true},
{map[string][]string{"": ips}, false},
{map[string][]string{"weave": ips}, true},
{map[string][]string{"weave": []string{}}, false},
{map[string][]string{"mesos": ips}, false},
{map[string][]string{"dc.mesos": ips}, false},
{map[string][]string{"acdc.mesos": ips}, true},
{map[string][]string{"site.dc.mesos": ips}, false},
{map[string][]string{"abc.com": ips, "com": ips}, false},
{map[string][]string{"abc.com": ips, "bc.com": ips}, true},
} {
validateZone(t, i+1, tc, fn)
}
}

type zoneValidationTest struct {
in map[string][]string
valid bool
}

func validateZone(t *testing.T, i int, tc zoneValidationTest,
f func(map[string][]string) error) {
switch err := f(tc.in); {
case (err == nil && tc.valid) || (err != nil && !tc.valid):
return // valid
case tc.valid:
t.Fatalf("test %d failed, unexpected error validating zone resolvers "+
"%v: %v", i, tc.in, err)
default:
t.Fatalf("test %d failed, expected validation error for zone resolvers(%d)"+
" %v", i, len(tc.in), tc.in)
}
}
47 changes: 32 additions & 15 deletions resolver/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ type Resolver struct {
rs *records.RecordGenerator
rsLock sync.RWMutex
rng *rand.Rand
fwd exchanger.Forwarder
generatorOptions []records.Option
zoneFwds map[string]exchanger.Forwarder // map of zone -> forwarder
defaultFwd exchanger.Forwarder
}

// New returns a Resolver with the given version and configuration.
Expand All @@ -57,11 +58,17 @@ func New(version string, config records.Config) *Resolver {
timeout = time.Duration(config.Timeout) * time.Second
}

rs := config.Resolvers
if !config.ExternalOn {
rs = rs[:0]
r.zoneFwds = make(map[string]exchanger.Forwarder)
if config.ExternalOn {
for zone, resolvers := range config.ZoneResolvers {
r.zoneFwds[zone] = exchanger.NewForwarder(resolvers, exchangers(timeout, "udp", "tcp"))
}
r.defaultFwd = exchanger.NewForwarder(
config.Resolvers, exchangers(timeout, "udp", "tcp"))
} else {
r.defaultFwd = exchanger.NewForwarder(
make([]string, 0), exchangers(timeout, "udp", "tcp"))
}
r.fwd = exchanger.NewForwarder(rs, exchangers(timeout, "udp", "tcp"))

return r
}
Expand Down Expand Up @@ -101,8 +108,15 @@ func (res *Resolver) records() *records.RecordGenerator {
func (res *Resolver) LaunchDNS() <-chan error {
// Handers for Mesos requests
dns.HandleFunc(res.config.Domain+".", panicRecover(res.HandleMesos))
// Handler for nonMesos requests
dns.HandleFunc(".", panicRecover(res.HandleNonMesos))
// Handlers for nonMesos requests
for zone, fwd := range res.zoneFwds {
dns.HandleFunc(
zone+".",
panicRecover(res.HandleNonMesos(fwd)))
}
dns.HandleFunc(
".",
panicRecover(res.HandleNonMesos(res.defaultFwd)))

errCh := make(chan error, 2)
_, e1 := res.Serve("tcp")
Expand Down Expand Up @@ -261,15 +275,18 @@ func shuffleAnswers(rng *rand.Rand, answers []dns.RR) []dns.RR {

// HandleNonMesos handles non-mesos queries by forwarding to configured
// external DNS servers.
func (res *Resolver) HandleNonMesos(w dns.ResponseWriter, r *dns.Msg) {
logging.CurLog.NonMesosRequests.Inc()
m, err := res.fwd(r, w.RemoteAddr().Network())
if err != nil {
m = new(dns.Msg).SetRcode(r, rcode(err))
} else if len(m.Answer) == 0 {
logging.CurLog.NonMesosNXDomain.Inc()
func (res *Resolver) HandleNonMesos(fwd exchanger.Forwarder) func(
dns.ResponseWriter, *dns.Msg) {
return func(w dns.ResponseWriter, r *dns.Msg) {
logging.CurLog.NonMesosRequests.Inc()
m, err := fwd(r, w.RemoteAddr().Network())
if err != nil {
m = new(dns.Msg).SetRcode(r, rcode(err))
} else if len(m.Answer) == 0 {
logging.CurLog.NonMesosNXDomain.Inc()
}
reply(w, m)
}
reply(w, m)
}

func rcode(err error) int {
Expand Down
4 changes: 2 additions & 2 deletions resolver/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func runHandlers() error {
if err != nil {
return err
}
res.fwd = func(m *dns.Msg, net string) (*dns.Msg, error) {
fwd := func(m *dns.Msg, net string) (*dns.Msg, error) {
rr1, err := res.formatA("google.com.", "1.1.1.1")
if err != nil {
return nil, err
Expand Down Expand Up @@ -218,7 +218,7 @@ func runHandlers() error {
"ns1.mesos", "root.ns1.mesos", 60))),
},
{
res.HandleNonMesos,
res.HandleNonMesos(fwd),
Message(
Question("google.com.", dns.TypeA),
Header(false, dns.RcodeSuccess),
Expand Down

0 comments on commit fb1dcdd

Please sign in to comment.