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:
parent
15d7340a75
commit
93b8a4e8d8
|
@ -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/"
|
||||
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
30
agent/dns.go
30
agent/dns.go
|
@ -81,6 +81,7 @@ type DNSServer struct {
|
|||
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
|
||||
|
@ -92,12 +93,14 @@ 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,
|
||||
altDomain: altDomain,
|
||||
logger: a.logger,
|
||||
}
|
||||
cfg, err := GetDNSConfig(a.config)
|
||||
|
@ -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
|
||||
|
|
|
@ -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(), `
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue