diff --git a/agent/config/builder.go b/agent/config/builder.go index 9234ecfd7..c37d8bdaa 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 8bc3dcdaf..2a873da82 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 23555a25f..fb6e034cd 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 3f833e4fb..1c199ab9b 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 e6a2fd148..79560ce80 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 fd9df8ebf..36ad19e20 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 50ff428ac..bc6078d0a 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 fdb4f65e7..3b970e3b2 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 8766e2ff5..94b0ee7a9 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 afa51d85a..b2cdbe1fa 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 8fe81f515..1510f675b 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