dns: support alt domains for dns resolution (#5940)

this adds an option for an alt domain to be used with dns while migrating to a new consul domain.
This commit is contained in:
Akshay Ganeshen 2019-06-27 06:00:37 -04:00 committed by Hans Hasselberg
parent 15d7340a75
commit 93b8a4e8d8
8 changed files with 273 additions and 15 deletions

View File

@ -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/"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
* <a name="_alt_domain"></a><a href="#_alt_domain">`-alt-domain`</a> - 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.
* <a name="_enable_script_checks"></a><a
href="#_enable_script_checks">`-enable-script-checks`</a> This controls
whether [health checks that execute scripts](/docs/agent/checks.html) are