diff --git a/agent/config/builder.go b/agent/config/builder.go index 6048dab92..c96183961 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 79d274d0d..48227d955 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:"additional_node_meta_txt,omitempty" hcl:"additional_node_meta_txt" mapstructure:"additional_node_meta_txt"` } type HTTPConfig struct { diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 66e7e79e7..c1df5a2d5 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/dns.go b/agent/dns.go index 1d3c46d97..1b8c2e20c 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, } } @@ -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 := true + + if node == nil { + node_meta_txt = false + } else if qType == dns.TypeANY { + // Since any RR type is requested allow the configuration to + // determine whether or not node meta gets added as TXT records + node_meta_txt = d.config.NodeMetaTXT + } else if qType != dns.TypeTXT { + // qType isn't TXT or ANY so avoid emitting the TXT records + node_meta_txt = false + } + + 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 diff --git a/agent/dns_test.go b/agent/dns_test.go index 41aca8e0e..454d598c3 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 = { 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() @@ -513,6 +558,42 @@ func TestDNS_NodeLookup_ANY(t *testing.T) { } +func TestDNS_NodeLookup_ANY_SuppressTXT(t *testing.T) { + a := NewTestAgent(t.Name(), `dns_config = { 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 + }, + } + verify.Values(t, "answer", in.Answer, wantAnswer) +} + func TestDNS_EDNS0(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") @@ -4613,6 +4694,93 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) { } } +func TestDNS_ServiceLookup_MetaTXT(t *testing.T) { + a := NewTestAgent(t.Name(), `dns_config = { 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 = { 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 4badb25ca..1e1e274b9 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -777,6 +777,11 @@ 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). + + * `additional_node_meta_txt` - If set + to false, node metadata will not be synthesized into TXT records and returned except for queries specifically for + TXT records. By default, TXT records will be generated for node queries with an ANY query type or for SRV queries + of services. * `domain` Equivalent to the [`-domain` command-line flag](#_domain).