From 251156eb68a6ad002f618837811dad632a99e109 Mon Sep 17 00:00:00 2001 From: Pierre Souchay Date: Wed, 10 Oct 2018 21:50:56 +0200 Subject: [PATCH] Added SOA configuration for DNS settings. (#4714) This will allow to fine TUNE SOA settings sent by Consul in DNS responses, for instance to be able to control negative ttl. Will fix: https://github.com/hashicorp/consul/issues/4713 # Example Override all settings: * min_ttl: 0 => 60s * retry: 600 (10m) => 300s (5 minutes), * expire: 86400 (24h) => 43200 (12h) * refresh: 3600 (1h) => 1800 (30 minutes) ``` consul agent -dev -hcl 'dns_config={soa={min_ttl=60,retry=300,expire=43200,refresh=1800}}' ``` Result: ``` dig +multiline @localhost -p 8600 service.consul ; <<>> DiG 9.12.1 <<>> +multiline @localhost -p 8600 service.consul ; (2 servers found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 36557 ;; flags: qr aa rd; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1 ;; WARNING: recursion requested but not available ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;service.consul. IN A ;; AUTHORITY SECTION: consul. 0 IN SOA ns.consul. hostmaster.consul. ( 1537959133 ; serial 1800 ; refresh (30 minutes) 300 ; retry (5 minutes) 43200 ; expire (12 hours) 60 ; minimum (1 minute) ) ;; Query time: 4 msec ;; SERVER: 127.0.0.1#8600(127.0.0.1) ;; WHEN: Wed Sep 26 12:52:13 CEST 2018 ;; MSG SIZE rcvd: 93 ``` --- agent/config/builder.go | 17 ++++++++++ agent/config/config.go | 9 ++++++ agent/config/runtime.go | 11 +++++++ agent/config/runtime_test.go | 8 +++++ agent/consul/catalog_endpoint_test.go | 4 +-- agent/dns.go | 32 +++++++++++++------ agent/dns_test.go | 33 ++++++++++++++++++++ agent/ui_endpoint_test.go | 2 +- api/agent_test.go | 2 ++ website/source/docs/agent/options.html.md | 22 +++++++++++++ website/source/docs/guides/dns-cache.html.md | 5 +++ 11 files changed, 133 insertions(+), 12 deletions(-) diff --git a/agent/config/builder.go b/agent/config/builder.go index 9234ecfd74e0..c37d8bdaacc2 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -301,6 +301,22 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { dnsServiceTTL[k] = b.durationVal(fmt.Sprintf("dns_config.service_ttl[%q]", k), &v) } + soa := RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0} + if c.DNS.SOA != nil { + if c.DNS.SOA.Expire != nil { + soa.Expire = *c.DNS.SOA.Expire + } + if c.DNS.SOA.Minttl != nil { + soa.Minttl = *c.DNS.SOA.Minttl + } + if c.DNS.SOA.Refresh != nil { + soa.Refresh = *c.DNS.SOA.Refresh + } + if c.DNS.SOA.Retry != nil { + soa.Retry = *c.DNS.SOA.Retry + } + } + leaveOnTerm := !b.boolVal(c.ServerMode) if c.LeaveOnTerm != nil { leaveOnTerm = b.boolVal(c.LeaveOnTerm) @@ -649,6 +665,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { DNSRecursorTimeout: b.durationVal("recursor_timeout", c.DNS.RecursorTimeout), DNSRecursors: dnsRecursors, DNSServiceTTL: dnsServiceTTL, + DNSSOA: soa, DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit), DNSNodeMetaTXT: b.boolValWithDefault(c.DNS.NodeMetaTXT, true), diff --git a/agent/config/config.go b/agent/config/config.go index 8bc3dcdaf45a..2a873da8298c 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -521,6 +521,14 @@ type ConnectProxyDefaults struct { Config map[string]interface{} `json:"config,omitempty" hcl:"config" mapstructure:"config"` } +// SOA is the configuration of SOA for DNS +type SOA struct { + Refresh *uint32 `json:"refresh,omitempty" hcl:"refresh" mapstructure:"refresh"` + Retry *uint32 `json:"retry,omitempty" hcl:"retry" mapstructure:"retry"` + Expire *uint32 `json:"expire,omitempty" hcl:"expire" mapstructure:"expire"` + Minttl *uint32 `json:"min_ttl,omitempty" hcl:"min_ttl" mapstructure:"min_ttl"` +} + type DNS struct { AllowStale *bool `json:"allow_stale,omitempty" hcl:"allow_stale" mapstructure:"allow_stale"` ARecordLimit *int `json:"a_record_limit,omitempty" hcl:"a_record_limit" mapstructure:"a_record_limit"` @@ -533,6 +541,7 @@ type DNS struct { 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"` + SOA *SOA `json:"soa,omitempty" hcl:"soa" mapstructure:"soa"` } type HTTPConfig struct { diff --git a/agent/config/runtime.go b/agent/config/runtime.go index 23555a25fa2a..fb6e034cd4ea 100644 --- a/agent/config/runtime.go +++ b/agent/config/runtime.go @@ -16,6 +16,13 @@ import ( "golang.org/x/time/rate" ) +type RuntimeSOAConfig struct { + Refresh uint32 // 3600 by default + Retry uint32 // 600 + Expire uint32 // 86400 + Minttl uint32 // 0, +} + // RuntimeConfig specifies the configuration the consul agent actually // uses. Is is derived from one or more Config structures which can come // from files, flags and/or environment variables. @@ -538,6 +545,10 @@ type RuntimeConfig struct { // flags: -dns-port int DNSPort int + // DNSSOA is the settings applied for DNS SOA + // hcl: soa {} + DNSSOA RuntimeSOAConfig + // DataDir is the path to the directory where the local state is stored. // // hcl: data_dir = string diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 3f833e4fb4f8..1c199ab9be7e 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -4110,6 +4110,7 @@ func TestFullConfig(t *testing.T) { DNSPort: 7001, DNSRecursorTimeout: 4427 * time.Second, DNSRecursors: []string{"63.38.39.58", "92.49.18.18"}, + DNSSOA: RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0}, DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second}, DNSUDPAnswerLimit: 29909, DNSNodeMetaTXT: true, @@ -4754,6 +4755,7 @@ func TestSanitize(t *testing.T) { &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.UDPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, }, + DNSSOA: RuntimeSOAConfig{Refresh: 3600, Retry: 600, Expire: 86400, Minttl: 0}, HTTPAddrs: []net.Addr{ &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.UnixAddr{Name: "/var/run/foo"}, @@ -4894,6 +4896,12 @@ func TestSanitize(t *testing.T) { "DNSRecursorTimeout": "0s", "DNSRecursors": [], "DNSServiceTTL": {}, + "DNSSOA": { + "Refresh": 3600, + "Retry": 600, + "Expire": 86400, + "Minttl": 0 + }, "DNSUDPAnswerLimit": 0, "DataDir": "", "Datacenter": "", diff --git a/agent/consul/catalog_endpoint_test.go b/agent/consul/catalog_endpoint_test.go index e6a2fd148b57..79560ce808e0 100644 --- a/agent/consul/catalog_endpoint_test.go +++ b/agent/consul/catalog_endpoint_test.go @@ -940,8 +940,8 @@ func TestCatalog_ListNodes_StaleRead(t *testing.T) { // Try to join joinLAN(t, s2, s1) - testrpc.WaitForLeader(t, s1.RPC, "dc1") - testrpc.WaitForLeader(t, s2.RPC, "dc1") + testrpc.WaitForTestAgent(t, s1.RPC, "dc1") + testrpc.WaitForTestAgent(t, s2.RPC, "dc1") // Use the follower as the client var codec rpc.ClientCodec diff --git a/agent/dns.go b/agent/dns.go index fd9df8ebf914..36ad19e20d38 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -39,6 +39,13 @@ const ( var InvalidDnsRe = regexp.MustCompile(`[^A-Za-z0-9\\-]+`) +type dnsSOAConfig struct { + Refresh uint32 // 3600 by default + Retry uint32 // 600 + Expire uint32 // 86400 + Minttl uint32 // 0, +} + type dnsConfig struct { AllowStale bool Datacenter string @@ -53,6 +60,7 @@ type dnsConfig struct { UDPAnswerLimit int ARecordLimit int NodeMetaTXT bool + dnsSOAConfig dnsSOAConfig } // DNSServer is used to wrap an Agent and expose various @@ -97,6 +105,7 @@ func NewDNSServer(a *Agent) (*DNSServer, error) { return srv, nil } +// GetDNSConfig takes global config and creates the config used by DNS server func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig { return &dnsConfig{ AllowStale: conf.DNSAllowStale, @@ -112,6 +121,12 @@ func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig { ServiceTTL: conf.DNSServiceTTL, UDPAnswerLimit: conf.DNSUDPAnswerLimit, NodeMetaTXT: conf.DNSNodeMetaTXT, + dnsSOAConfig: dnsSOAConfig{ + Expire: conf.DNSSOA.Expire, + Minttl: conf.DNSSOA.Minttl, + Refresh: conf.DNSSOA.Refresh, + Retry: conf.DNSSOA.Retry, + }, } } @@ -349,17 +364,16 @@ func (d *DNSServer) soa() *dns.SOA { Name: d.domain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, - Ttl: 0, + // Has to be consistent with MinTTL to avoid invalidation + Ttl: d.config.dnsSOAConfig.Minttl, }, - Ns: "ns." + d.domain, - Serial: uint32(time.Now().Unix()), - - // todo(fs): make these configurable + Ns: "ns." + d.domain, + Serial: uint32(time.Now().Unix()), Mbox: "hostmaster." + d.domain, - Refresh: 3600, - Retry: 600, - Expire: 86400, - Minttl: 0, + Refresh: d.config.dnsSOAConfig.Refresh, + Retry: d.config.dnsSOAConfig.Retry, + Expire: d.config.dnsSOAConfig.Expire, + Minttl: d.config.dnsSOAConfig.Minttl, } } diff --git a/agent/dns_test.go b/agent/dns_test.go index 50ff428acd27..bc6078d0ab82 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -1115,6 +1115,39 @@ func TestDNS_ServiceReverseLookup_CustomDomain(t *testing.T) { } } +func TestDNS_SOA_Settings(t *testing.T) { + t.Parallel() + testSoaWithConfig := func(config string, ttl, expire, refresh, retry uint) { + a := NewTestAgent(t.Name(), config) + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + // lookup a non-existing node, we should receive a SOA + m := new(dns.Msg) + m.SetQuestion("nofoo.node.dc1.consul.", dns.TypeANY) + + c := new(dns.Client) + in, _, err := c.Exchange(m, a.DNSAddr()) + require.NoError(t, err) + require.Len(t, in.Ns, 1) + soaRec, ok := in.Ns[0].(*dns.SOA) + require.True(t, ok, "NS RR is not a SOA record") + require.Equal(t, uint32(ttl), soaRec.Minttl) + require.Equal(t, uint32(expire), soaRec.Expire) + require.Equal(t, uint32(refresh), soaRec.Refresh) + require.Equal(t, uint32(retry), soaRec.Retry) + require.Equal(t, uint32(ttl), soaRec.Hdr.Ttl) + } + // Default configuration + testSoaWithConfig("", 0, 86400, 3600, 600) + // Override all settings + testSoaWithConfig("dns_config={soa={min_ttl=60,expire=43200,refresh=1800,retry=300}}", 60, 43200, 1800, 300) + // Override partial settings + testSoaWithConfig("dns_config={soa={min_ttl=60,expire=43200}}", 60, 43200, 3600, 600) + // Override partial settings, part II + testSoaWithConfig("dns_config={soa={refresh=1800,retry=300}}", 0, 86400, 1800, 300) +} + func TestDNS_ServiceReverseLookupNodeAddress(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index fdb4f65e7b6e..3b970e3b2b3f 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -68,7 +68,7 @@ func TestUiNodes(t *testing.T) { t.Parallel() a := NewTestAgent(t.Name(), "") defer a.Shutdown() - testrpc.WaitForLeader(t, a.RPC, "dc1") + testrpc.WaitForTestAgent(t, a.RPC, "dc1") args := &structs.RegisterRequest{ Datacenter: "dc1", diff --git a/api/agent_test.go b/api/agent_test.go index 8766e2ff563e..94b0ee7a9167 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -1242,6 +1242,7 @@ func TestAPI_AgentConnectCARoots_list(t *testing.T) { defer s.Stop() agent := c.Agent() + s.WaitForSerfCheck(t) list, meta, err := agent.ConnectCARoots(nil) require.NoError(err) require.True(meta.LastIndex > 0) @@ -1286,6 +1287,7 @@ func TestAPI_AgentConnectAuthorize(t *testing.T) { defer s.Stop() agent := c.Agent() + s.WaitForSerfCheck(t) params := &AgentAuthorizeParams{ Target: "foo", ClientCertSerial: "fake", diff --git a/website/source/docs/agent/options.html.md b/website/source/docs/agent/options.html.md index afa51d85ac86..b2cdbe1fa6dc 100644 --- a/website/source/docs/agent/options.html.md +++ b/website/source/docs/agent/options.html.md @@ -884,6 +884,28 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass 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. + * `soa` Allow to tune the setting set up in SOA. + Non specified values fallback to their default values, all values are integers and + expressed as seconds. + + The following settings are available: + + * expire - + Configure SOA Expire duration in seconds, default value is 86400, ie: 24 hours. + + * `min_ttl` - + Configure SOA DNS minimum TTL. + As explained in [RFC-2308](https://tools.ietf.org/html/rfc2308) this also controls + negative cache TTL in most implementations. Default value is 0, ie: no minimum + delay or negative TTL. + + * refresh - + Configure SOA Refresh duration in seconds, default value is `3600`, ie: 1 hour. + + * retry - + Configures the Retry duration expressed in seconds, default value is + 600, ie: 10 minutes. + * `domain` Equivalent to the [`-domain` command-line flag](#_domain). diff --git a/website/source/docs/guides/dns-cache.html.md b/website/source/docs/guides/dns-cache.html.md index 8fe81f515224..1510f675b433 100644 --- a/website/source/docs/guides/dns-cache.html.md +++ b/website/source/docs/guides/dns-cache.html.md @@ -65,6 +65,11 @@ client and Consul and set the cache values appropriately. In many cases "appropriately" simply is turning negative response caching off to get the best recovery time when a service becomes available again. +With versions of Consul greater than 1.3.0, it is now possible to tune SOA +responses and modify the negative TTL cache for some resolvers. It can +be achieved using the [`soa.min_ttl`](/docs/agent/options.html#soa_min_ttl) +configuration within the [`soa`](/docs/agent/options.html#soa) configuration. + ## TTL Values