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