diff --git a/agent/config/builder.go b/agent/config/builder.go index 8e510d8cb..53d4abbb5 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -496,6 +496,9 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { } } + datacenter := strings.ToLower(b.stringVal(c.Datacenter)) + altDomain := b.stringVal(c.DNSAltDomain) + // Create the default set of tagged addresses. if c.TaggedAddresses == nil { c.TaggedAddresses = make(map[string]string) @@ -588,8 +591,6 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { }) } - datacenter := strings.ToLower(b.stringVal(c.Datacenter)) - aclsEnabled := false primaryDatacenter := strings.ToLower(b.stringVal(c.PrimaryDatacenter)) if c.ACLDatacenter != nil { @@ -727,6 +728,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { DNSARecordLimit: b.intVal(c.DNS.ARecordLimit), DNSDisableCompression: b.boolVal(c.DNS.DisableCompression), DNSDomain: b.stringVal(c.DNSDomain), + DNSAltDomain: altDomain, DNSEnableTruncate: b.boolVal(c.DNS.EnableTruncate), DNSMaxStale: b.durationVal("dns_config.max_stale", c.DNS.MaxStale), DNSNodeTTL: b.durationVal("dns_config.node_ttl", c.DNS.NodeTTL), @@ -964,6 +966,9 @@ func (b *Builder) Validate(rt RuntimeConfig) error { return fmt.Errorf("DNS recursor address cannot be 0.0.0.0, :: or [::]") } } + if !isValidAltDomain(rt.DNSAltDomain, rt.Datacenter) { + return fmt.Errorf("alt_domain cannot start with {service,connect,node,query,addr,%s}", rt.Datacenter) + } if rt.Bootstrap && !rt.ServerMode { return fmt.Errorf("'bootstrap = true' requires 'server = true'") } @@ -1686,6 +1691,18 @@ func isUnixAddr(a net.Addr) bool { return ok } +// isValidAltDomain returns true if the given domain is not prefixed +// by keywords used when dispatching DNS requests +func isValidAltDomain(domain, datacenter string) bool { + reAltDomain := regexp.MustCompile( + fmt.Sprintf( + "^(service|connect|node|query|addr|%s)\\.(%s\\.)?", + datacenter, datacenter, + ), + ) + return !reAltDomain.MatchString(domain) +} + // UIPathBuilder checks to see if there was a path set // If so, adds beginning and trailing slashes to UI path func UIPathBuilder(UIContentString string) string { @@ -1697,5 +1714,4 @@ func UIPathBuilder(UIContentString string) string { } return "/ui/" - } diff --git a/agent/config/config.go b/agent/config/config.go index a38bb0ae0..16392438a 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -192,6 +192,7 @@ type Config struct { Connect Connect `json:"connect,omitempty" hcl:"connect" mapstructure:"connect"` DNS DNS `json:"dns_config,omitempty" hcl:"dns_config" mapstructure:"dns_config"` DNSDomain *string `json:"domain,omitempty" hcl:"domain" mapstructure:"domain"` + DNSAltDomain *string `json:"alt_domain,omitempty" hcl:"alt_domain" mapstructure:"alt_domain"` DNSRecursors []string `json:"recursors,omitempty" hcl:"recursors" mapstructure:"recursors"` DataDir *string `json:"data_dir,omitempty" hcl:"data_dir" mapstructure:"data_dir"` Datacenter *string `json:"datacenter,omitempty" hcl:"datacenter" mapstructure:"datacenter"` diff --git a/agent/config/flags.go b/agent/config/flags.go index d25896840..b6cc5966b 100644 --- a/agent/config/flags.go +++ b/agent/config/flags.go @@ -71,6 +71,7 @@ func AddFlags(fs *flag.FlagSet, f *Flags) { add(&f.Config.DisableKeyringFile, "disable-keyring-file", "Disables the backing up of the keyring to a file.") add(&f.Config.Ports.DNS, "dns-port", "DNS port to use.") add(&f.Config.DNSDomain, "domain", "Domain to use for DNS interface.") + add(&f.Config.DNSAltDomain, "alt-domain", "Alternate domain to use for DNS interface.") add(&f.Config.EnableScriptChecks, "enable-script-checks", "Enables health check scripts.") add(&f.Config.EnableLocalScriptChecks, "enable-local-script-checks", "Enables health check scripts from configuration file.") add(&f.Config.HTTPConfig.AllowWriteHTTPFrom, "allow-write-http-from", "Only allow write endpoint calls from given network. CIDR format, can be specified multiple times.") diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 528428775..9555b1a84 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -259,6 +259,14 @@ type RuntimeConfig struct { // flag: -domain string DNSDomain string + // DNSAltDomain can be set to support resolution on an additional + // consul domain. Should end with a dot. + // If left blank, only the primary domain will be used. + // + // hcl: alt_domain = string + // flag: -alt-domain string + DNSAltDomain string + // DNSEnableTruncate is used to enable setting the truncate // flag for UDP DNS queries. This allows unmodified // clients to re-query the consul server using TCP diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index ef5602ae3..9ae1369b8 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -345,6 +345,53 @@ func TestConfigFlagsAndEdgecases(t *testing.T) { rt.DataDir = dataDir }, }, + { + desc: "-alt-domain", + args: []string{ + `-alt-domain=alt`, + `-data-dir=` + dataDir, + }, + patch: func(rt *RuntimeConfig) { + rt.DNSAltDomain = "alt" + rt.DataDir = dataDir + }, + }, + { + desc: "-alt-domain can't be prefixed by DC", + args: []string{ + `-datacenter=a`, + `-alt-domain=a.alt`, + `-data-dir=` + dataDir, + }, + err: "alt_domain cannot start with {service,connect,node,query,addr,a}", + }, + { + desc: "-alt-domain can't be prefixed by service", + args: []string{ + `-alt-domain=service.alt`, + `-data-dir=` + dataDir, + }, + err: "alt_domain cannot start with {service,connect,node,query,addr,dc1}", + }, + { + desc: "-alt-domain can be prefixed by non-keywords", + args: []string{ + `-alt-domain=mydomain.alt`, + `-data-dir=` + dataDir, + }, + patch: func(rt *RuntimeConfig) { + rt.DNSAltDomain = "mydomain.alt" + rt.DataDir = dataDir + }, + }, + { + desc: "-alt-domain can't be prefixed by DC", + args: []string{ + `-alt-domain=dc1.alt`, + `-data-dir=` + dataDir, + }, + err: "alt_domain cannot start with {service,connect,node,query,addr,dc1}", + }, { desc: "-enable-script-checks", args: []string{ @@ -3163,6 +3210,7 @@ func TestFullConfig(t *testing.T) { "discard_check_output": true, "discovery_max_stale": "5s", "domain": "7W1xXSqd", + "alt_domain": "1789hsd", "dns_config": { "allow_stale": true, "a_record_limit": 29907, @@ -3741,6 +3789,7 @@ func TestFullConfig(t *testing.T) { discard_check_output = true discovery_max_stale = "5s" domain = "7W1xXSqd" + alt_domain = "1789hsd" dns_config { allow_stale = true a_record_limit = 29907 @@ -4396,6 +4445,7 @@ func TestFullConfig(t *testing.T) { DNSAllowStale: true, DNSDisableCompression: true, DNSDomain: "7W1xXSqd", + DNSAltDomain: "1789hsd", DNSEnableTruncate: true, DNSMaxStale: 29685 * time.Second, DNSNodeTTL: 7084 * time.Second, @@ -5203,6 +5253,7 @@ func TestSanitize(t *testing.T) { "DNSAllowStale": false, "DNSDisableCompression": false, "DNSDomain": "", + "DNSAltDomain": "", "DNSEnableTruncate": false, "DNSMaxStale": "0s", "DNSNodeMetaTXT": false, diff --git a/agent/dns.go b/agent/dns.go index a4bffa18e..5d0fd494e 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -78,10 +78,11 @@ type dnsConfig struct { // service discovery endpoints using a DNS interface. type DNSServer struct { *dns.Server - agent *Agent - mux *dns.ServeMux - domain string - logger *log.Logger + agent *Agent + mux *dns.ServeMux + domain string + altDomain string + logger *log.Logger // config stores the config as an atomic value (for hot-reloading). It is always of type *dnsConfig config atomic.Value @@ -92,13 +93,15 @@ type DNSServer struct { } func NewDNSServer(a *Agent) (*DNSServer, error) { - // Make sure domain is FQDN, make it case insensitive for ServeMux + // Make sure domains are FQDN, make them case insensitive for ServeMux domain := dns.Fqdn(strings.ToLower(a.config.DNSDomain)) + altDomain := dns.Fqdn(strings.ToLower(a.config.DNSAltDomain)) srv := &DNSServer{ - agent: a, - domain: domain, - logger: a.logger, + agent: a, + domain: domain, + altDomain: altDomain, + logger: a.logger, } cfg, err := GetDNSConfig(a.config) if err != nil { @@ -183,6 +186,9 @@ func (d *DNSServer) ListenAndServe(network, addr string, notif func()) error { d.mux = dns.NewServeMux() d.mux.HandleFunc("arpa.", d.handlePtr) d.mux.HandleFunc(d.domain, d.handleQuery) + if d.altDomain != "" { + d.mux.HandleFunc(d.altDomain, d.handleQuery) + } d.toggleRecursorHandlerFromConfig(cfg) d.Server = &dns.Server{ @@ -530,7 +536,7 @@ func (d *DNSServer) doDispatch(network string, remoteAddr net.Addr, req, resp *d // Get the QName without the domain suffix qName := strings.ToLower(dns.Fqdn(req.Question[0].Name)) - qName = strings.TrimSuffix(qName, d.domain) + qName = d.trimDomain(qName) // Split into the label parts labels := dns.SplitDomainName(qName) @@ -684,6 +690,20 @@ INVALID: return } +func (d *DNSServer) trimDomain(query string) string { + longer := d.domain + shorter := d.altDomain + + if len(shorter) > len(longer) { + longer, shorter = shorter, longer + } + + if strings.HasSuffix(query, longer) { + return strings.TrimSuffix(query, longer) + } + return strings.TrimSuffix(query, shorter) +} + // nodeLookup is used to handle a node query func (d *DNSServer) nodeLookup(cfg *dnsConfig, network, datacenter, node string, req, resp *dns.Msg, maxRecursionLevel int) { // Only handle ANY, A, AAAA, and TXT type requests @@ -1602,10 +1622,10 @@ func (d *DNSServer) handleRecurse(resp dns.ResponseWriter, req *dns.Msg) { // resolveCNAME is used to recursively resolve CNAME records func (d *DNSServer) resolveCNAME(cfg *dnsConfig, name string, maxRecursionLevel int) []dns.RR { // If the CNAME record points to a Consul address, resolve it internally - // Convert query to lowercase because DNS is case insensitive; d.domain is - // already converted + // Convert query to lowercase because DNS is case insensitive; d.domain and + // d.altDomain are already converted - if strings.HasSuffix(strings.ToLower(name), "."+d.domain) { + if ln := strings.ToLower(name); strings.HasSuffix(ln, "."+d.domain) || strings.HasSuffix(ln, "."+d.altDomain) { if maxRecursionLevel < 1 { d.logger.Printf("[ERR] dns: Infinite recursion detected for %s, won't perform any CNAME resolution.", name) return nil diff --git a/agent/dns_test.go b/agent/dns_test.go index fbb02d672..df2cc9587 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -5541,6 +5541,164 @@ func TestDNS_NonExistingLookupEmptyAorAAAA(t *testing.T) { } } +func TestDNS_AltDomains_Service(t *testing.T) { + t.Parallel() + a := NewTestAgent(t, t.Name(), ` + alt_domain = "test-domain." + `) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + // Register a node with a service. + { + args := &structs.RegisterRequest{ + Datacenter: "dc1", + Node: "test-node", + Address: "127.0.0.1", + Service: &structs.NodeService{ + Service: "db", + Tags: []string{"master"}, + Port: 12345, + }, + } + + var out struct{} + if err := a.RPC("Catalog.Register", args, &out); err != nil { + t.Fatalf("err: %v", err) + } + } + + questions := []string{ + "db.service.consul.", + "db.service.test-domain.", + "db.service.dc1.consul.", + "db.service.dc1.test-domain.", + } + + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSRV) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + srvRec, ok := in.Answer[0].(*dns.SRV) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + if srvRec.Port != 12345 { + t.Fatalf("Bad: %#v", srvRec) + } + if srvRec.Target != "test-node.node.dc1.consul." { + t.Fatalf("Bad: %#v", srvRec) + } + + aRec, ok := in.Extra[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.Hdr.Name != "test-node.node.dc1.consul." { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + if aRec.A.String() != "127.0.0.1" { + t.Fatalf("Bad: %#v", in.Extra[0]) + } + } +} + +func TestDNS_AltDomains_SOA(t *testing.T) { + t.Parallel() + a := NewTestAgent(t, t.Name(), ` + node_name = "test-node" + alt_domain = "test-domain." + `) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + questions := []string{ + "test-node.node.consul.", + "test-node.node.test-domain.", + } + + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeSOA) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("Bad: %#v", in) + } + + soaRec, ok := in.Answer[0].(*dns.SOA) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + if got, want := soaRec.Hdr.Name, "consul."; got != want { + t.Fatalf("SOA name invalid, got %q want %q", got, want) + } + if got, want := soaRec.Ns, "ns.consul."; got != want { + t.Fatalf("SOA ns invalid, got %q want %q", got, want) + } + } +} + +func TestDNS_AltDomains_Overlap(t *testing.T) { + // this tests the domain matching logic in DNSServer when encountering more + // than one potential match (i.e. ambiguous match) + // it should select the longer matching domain when dispatching + t.Parallel() + a := NewTestAgent(t, t.Name(), ` + node_name = "test-node" + alt_domain = "test.consul." + `) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + questions := []string{ + "test-node.node.consul.", + "test-node.node.test.consul.", + "test-node.node.dc1.consul.", + "test-node.node.dc1.test.consul.", + } + + for _, question := range questions { + m := new(dns.Msg) + m.SetQuestion(question, dns.TypeA) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + if err != nil { + t.Fatalf("err: %v", err) + } + + if len(in.Answer) != 1 { + t.Fatalf("failed to resolve ambiguous alt domain %q: %#v", question, in) + } + + aRec, ok := in.Answer[0].(*dns.A) + if !ok { + t.Fatalf("Bad: %#v", in.Answer[0]) + } + + if got, want := aRec.A.To4().String(), "127.0.0.1"; got != want { + t.Fatalf("A ip invalid, got %v want %v", got, want) + } + } +} + func TestDNS_PreparedQuery_AllowStale(t *testing.T) { t.Parallel() a := NewTestAgent(t, t.Name(), ` diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index 12b32b1b9..781a2f843 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -221,6 +221,9 @@ will exit with an error at startup. in the "consul." domain. This flag can be used to change that domain. All queries in this domain are assumed to be handled by Consul and will not be recursively resolved. +* `-alt-domain` - This flag allows Consul to respond to + DNS queries in an alternate domain, in addition to the primary domain. If unset, no alternate domain is used. + * `-enable-script-checks` This controls whether [health checks that execute scripts](/docs/agent/checks.html) are