diff --git a/agent/config/builder.go b/agent/config/builder.go
index 6048dab929..c961839619 100644
--- a/agent/config/builder.go
+++ b/agent/config/builder.go
@@ -592,6 +592,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
DNSRecursors: dnsRecursors,
DNSServiceTTL: dnsServiceTTL,
DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit),
+ DNSNodeMetaTXT: b.boolValWithDefault(c.DNS.NodeMetaTXT, true),
// HTTP
HTTPPort: httpPort,
@@ -1010,13 +1011,18 @@ func (b *Builder) serviceVal(v *ServiceDefinition) *structs.ServiceDefinition {
}
}
-func (b *Builder) boolVal(v *bool) bool {
+func (b *Builder) boolValWithDefault(v *bool, default_val bool) bool {
if v == nil {
- return false
+ return default_val
}
+
return *v
}
+func (b *Builder) boolVal(v *bool) bool {
+ return b.boolValWithDefault(v, false)
+}
+
func (b *Builder) durationVal(name string, v *string) (d time.Duration) {
if v == nil {
return 0
diff --git a/agent/config/config.go b/agent/config/config.go
index 79d274d0dd..13f7db6a96 100644
--- a/agent/config/config.go
+++ b/agent/config/config.go
@@ -360,6 +360,7 @@ type DNS struct {
RecursorTimeout *string `json:"recursor_timeout,omitempty" hcl:"recursor_timeout" mapstructure:"recursor_timeout"`
ServiceTTL map[string]string `json:"service_ttl,omitempty" hcl:"service_ttl" mapstructure:"service_ttl"`
UDPAnswerLimit *int `json:"udp_answer_limit,omitempty" hcl:"udp_answer_limit" mapstructure:"udp_answer_limit"`
+ NodeMetaTXT *bool `json:"enable_additional_node_meta_txt,omitempty" hcl:"enable_additional_node_meta_txt" mapstructure:"enable_additional_node_meta_txt"`
}
type HTTPConfig struct {
diff --git a/agent/config/runtime.go b/agent/config/runtime.go
index 66e7e79e7b..c1df5a2d56 100644
--- a/agent/config/runtime.go
+++ b/agent/config/runtime.go
@@ -281,6 +281,11 @@ type RuntimeConfig struct {
// hcl: dns_config { udp_answer_limit = int }
DNSUDPAnswerLimit int
+ // DNSNodeMetaTXT controls whether DNS queries will synthesize
+ // TXT records for the node metadata and add them when not specifically
+ // request (query type = TXT). If unset this will default to true
+ DNSNodeMetaTXT bool
+
// DNSRecursors can be set to allow the DNS servers to recursively
// resolve non-consul domains.
//
diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go
index 060215c355..191e8d1ea1 100644
--- a/agent/config/runtime_test.go
+++ b/agent/config/runtime_test.go
@@ -3371,6 +3371,7 @@ func TestFullConfig(t *testing.T) {
DNSRecursors: []string{"63.38.39.58", "92.49.18.18"},
DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second},
DNSUDPAnswerLimit: 29909,
+ DNSNodeMetaTXT: true,
DataDir: dataDir,
Datacenter: "rzo029wg",
DevMode: true,
@@ -4043,6 +4044,7 @@ func TestSanitize(t *testing.T) {
"DNSDomain": "",
"DNSEnableTruncate": false,
"DNSMaxStale": "0s",
+ "DNSNodeMetaTXT": false,
"DNSNodeTTL": "0s",
"DNSOnlyPassing": false,
"DNSPort": 0,
diff --git a/agent/dns.go b/agent/dns.go
index 1d3c46d972..993511d0d3 100644
--- a/agent/dns.go
+++ b/agent/dns.go
@@ -51,6 +51,7 @@ type dnsConfig struct {
ServiceTTL map[string]time.Duration
UDPAnswerLimit int
ARecordLimit int
+ NodeMetaTXT bool
}
// DNSServer is used to wrap an Agent and expose various
@@ -109,6 +110,7 @@ func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig {
SegmentName: conf.SegmentName,
ServiceTTL: conf.DNSServiceTTL,
UDPAnswerLimit: conf.DNSUDPAnswerLimit,
+ NodeMetaTXT: conf.DNSNodeMetaTXT,
}
}
@@ -374,7 +376,7 @@ func (d *DNSServer) nameservers(edns bool) (ns []dns.RR, extra []dns.RR) {
}
ns = append(ns, nsrr)
- glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns)
+ glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns, false)
extra = append(extra, glue...)
// don't provide more than 3 servers
@@ -582,7 +584,7 @@ RPC:
n := out.NodeServices.Node
edns := req.IsEdns0() != nil
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses)
- records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns)
+ records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns, true)
if records != nil {
resp.Answer = append(resp.Answer, records...)
}
@@ -610,7 +612,7 @@ func encodeKVasRFC1464(key, value string) (txt string) {
}
// formatNodeRecord takes a Node and returns an A, AAAA, TXT or CNAME record
-func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns bool) (records []dns.RR) {
+func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns, answer bool) (records []dns.RR) {
// Parse the IP
ip := net.ParseIP(addr)
var ipv4 net.IP
@@ -671,7 +673,20 @@ func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qTy
}
}
- if node != nil && (qType == dns.TypeANY || qType == dns.TypeTXT) {
+ node_meta_txt := false
+
+ if node == nil {
+ node_meta_txt = false
+ } else if answer {
+ node_meta_txt = true
+ } else {
+ // Use configuration when the TXT RR would
+ // end up in the Additional section of the
+ // DNS response
+ node_meta_txt = d.config.NodeMetaTXT
+ }
+
+ if node_meta_txt {
for key, value := range node.Meta {
txt := value
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") {
@@ -782,8 +797,8 @@ func (d *DNSServer) trimTCPResponse(req, resp *dns.Msg) (trimmed bool) {
originalNumRecords := len(resp.Answer)
// It is not possible to return more than 4k records even with compression
- // Since we are performing binary search it is not a big deal, but it
- // improves a bit performance, even with binary search
+ // Since we are performing binary search it is not a big deal, but it
+ // improves a bit performance, even with binary search
truncateAt := 4096
if req.Question[0].Qtype == dns.TypeSRV {
// More than 1024 SRV records do not fit in 64k
@@ -1143,7 +1158,7 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode
handled[addr] = struct{}{}
// Add the node record
- records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns)
+ records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns, true)
if records != nil {
resp.Answer = append(resp.Answer, records...)
count++
@@ -1192,7 +1207,7 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
}
// Add the extra record
- records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns)
+ records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns, false)
if len(records) > 0 {
// Use the node address if it doesn't differ from the service address
if addr == node.Node.Address {
diff --git a/agent/dns_test.go b/agent/dns_test.go
index 41aca8e0e2..a171132e27 100644
--- a/agent/dns_test.go
+++ b/agent/dns_test.go
@@ -472,6 +472,51 @@ func TestDNS_NodeLookup_TXT(t *testing.T) {
}
}
+func TestDNS_NodeLookup_TXT_DontSuppress(t *testing.T) {
+ a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
+ defer a.Shutdown()
+
+ args := &structs.RegisterRequest{
+ Datacenter: "dc1",
+ Node: "google",
+ Address: "127.0.0.1",
+ NodeMeta: map[string]string{
+ "rfc1035-00": "value0",
+ "key0": "value1",
+ },
+ }
+
+ var out struct{}
+ if err := a.RPC("Catalog.Register", args, &out); err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ m := new(dns.Msg)
+ m.SetQuestion("google.node.consul.", dns.TypeTXT)
+
+ c := new(dns.Client)
+ in, _, err := c.Exchange(m, a.DNSAddr())
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ // Should have the 1 TXT record reply
+ if len(in.Answer) != 2 {
+ t.Fatalf("Bad: %#v", in)
+ }
+
+ txtRec, ok := in.Answer[0].(*dns.TXT)
+ if !ok {
+ t.Fatalf("Bad: %#v", in.Answer[0])
+ }
+ if len(txtRec.Txt) != 1 {
+ t.Fatalf("Bad: %#v", in.Answer[0])
+ }
+ if txtRec.Txt[0] != "value0" && txtRec.Txt[0] != "key0=value1" {
+ t.Fatalf("Bad: %#v", in.Answer[0])
+ }
+}
+
func TestDNS_NodeLookup_ANY(t *testing.T) {
a := NewTestAgent(t.Name(), ``)
defer a.Shutdown()
@@ -510,7 +555,46 @@ func TestDNS_NodeLookup_ANY(t *testing.T) {
},
}
verify.Values(t, "answer", in.Answer, wantAnswer)
+}
+func TestDNS_NodeLookup_ANY_DontSuppressTXT(t *testing.T) {
+ a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
+ defer a.Shutdown()
+
+ args := &structs.RegisterRequest{
+ Datacenter: "dc1",
+ Node: "bar",
+ Address: "127.0.0.1",
+ NodeMeta: map[string]string{
+ "key": "value",
+ },
+ }
+
+ var out struct{}
+ if err := a.RPC("Catalog.Register", args, &out); err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ m := new(dns.Msg)
+ m.SetQuestion("bar.node.consul.", dns.TypeANY)
+
+ c := new(dns.Client)
+ in, _, err := c.Exchange(m, a.DNSAddr())
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ wantAnswer := []dns.RR{
+ &dns.A{
+ Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
+ A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
+ },
+ &dns.TXT{
+ Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xa},
+ Txt: []string{"key=value"},
+ },
+ }
+ verify.Values(t, "answer", in.Answer, wantAnswer)
}
func TestDNS_EDNS0(t *testing.T) {
@@ -4613,6 +4697,93 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) {
}
}
+func TestDNS_ServiceLookup_MetaTXT(t *testing.T) {
+ a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = true }`)
+ defer a.Shutdown()
+
+ args := &structs.RegisterRequest{
+ Datacenter: "dc1",
+ Node: "bar",
+ Address: "127.0.0.1",
+ NodeMeta: map[string]string{
+ "key": "value",
+ },
+ 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)
+ }
+
+ m := new(dns.Msg)
+ m.SetQuestion("db.service.consul.", dns.TypeSRV)
+
+ c := new(dns.Client)
+ in, _, err := c.Exchange(m, a.DNSAddr())
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ wantAdditional := []dns.RR{
+ &dns.A{
+ Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
+ A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
+ },
+ &dns.TXT{
+ Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xa},
+ Txt: []string{"key=value"},
+ },
+ }
+ verify.Values(t, "additional", in.Extra, wantAdditional)
+}
+
+func TestDNS_ServiceLookup_SuppressTXT(t *testing.T) {
+ a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
+ defer a.Shutdown()
+
+ // Register a node with a service.
+ args := &structs.RegisterRequest{
+ Datacenter: "dc1",
+ Node: "bar",
+ Address: "127.0.0.1",
+ NodeMeta: map[string]string{
+ "key": "value",
+ },
+ 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)
+ }
+
+ m := new(dns.Msg)
+ m.SetQuestion("db.service.consul.", dns.TypeSRV)
+
+ c := new(dns.Client)
+ in, _, err := c.Exchange(m, a.DNSAddr())
+ if err != nil {
+ t.Fatalf("err: %v", err)
+ }
+
+ wantAdditional := []dns.RR{
+ &dns.A{
+ Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
+ A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
+ },
+ }
+ verify.Values(t, "additional", in.Extra, wantAdditional)
+}
+
func TestDNS_AddressLookup(t *testing.T) {
t.Parallel()
a := NewTestAgent(t.Name(), "")
diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md
index 4badb25ca8..3f259e9ef5 100644
--- a/website/source/docs/agent/options.html.md
+++ b/website/source/docs/agent/options.html.md
@@ -777,6 +777,12 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
[RFC 6724](https://tools.ietf.org/html/rfc6724) and as a result it should
be increasingly uncommon to need to change this value with modern
resolvers).
+
+ * `enable_additional_node_meta_txt` -
+ When set to true, Consul will add TXT records for Node metadata into the Additional section of the DNS responses for several
+ query types such as SRV queries. When set to false those records are emitted. This does not impact the behavior of those
+ same TXT records when they would be added to the Answer section of the response like when querying with type TXT or ANY. This
+ defaults to true.
* `domain` Equivalent to the
[`-domain` command-line flag](#_domain).