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
```
This commit is contained in:
Pierre Souchay 2018-10-10 21:50:56 +02:00 committed by Matt Keeler
parent 391dbcf8dd
commit 251156eb68
11 changed files with 133 additions and 12 deletions

View File

@ -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) 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) leaveOnTerm := !b.boolVal(c.ServerMode)
if c.LeaveOnTerm != nil { if c.LeaveOnTerm != nil {
leaveOnTerm = b.boolVal(c.LeaveOnTerm) 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), DNSRecursorTimeout: b.durationVal("recursor_timeout", c.DNS.RecursorTimeout),
DNSRecursors: dnsRecursors, DNSRecursors: dnsRecursors,
DNSServiceTTL: dnsServiceTTL, DNSServiceTTL: dnsServiceTTL,
DNSSOA: soa,
DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit), DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit),
DNSNodeMetaTXT: b.boolValWithDefault(c.DNS.NodeMetaTXT, true), DNSNodeMetaTXT: b.boolValWithDefault(c.DNS.NodeMetaTXT, true),

View File

@ -521,6 +521,14 @@ type ConnectProxyDefaults struct {
Config map[string]interface{} `json:"config,omitempty" hcl:"config" mapstructure:"config"` 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 { type DNS struct {
AllowStale *bool `json:"allow_stale,omitempty" hcl:"allow_stale" mapstructure:"allow_stale"` 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"` 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"` 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"` 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"` 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 { type HTTPConfig struct {

View File

@ -16,6 +16,13 @@ import (
"golang.org/x/time/rate" "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 // RuntimeConfig specifies the configuration the consul agent actually
// uses. Is is derived from one or more Config structures which can come // uses. Is is derived from one or more Config structures which can come
// from files, flags and/or environment variables. // from files, flags and/or environment variables.
@ -538,6 +545,10 @@ type RuntimeConfig struct {
// flags: -dns-port int // flags: -dns-port int
DNSPort 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. // DataDir is the path to the directory where the local state is stored.
// //
// hcl: data_dir = string // hcl: data_dir = string

View File

@ -4110,6 +4110,7 @@ func TestFullConfig(t *testing.T) {
DNSPort: 7001, DNSPort: 7001,
DNSRecursorTimeout: 4427 * time.Second, DNSRecursorTimeout: 4427 * time.Second,
DNSRecursors: []string{"63.38.39.58", "92.49.18.18"}, 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}, DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second},
DNSUDPAnswerLimit: 29909, DNSUDPAnswerLimit: 29909,
DNSNodeMetaTXT: true, DNSNodeMetaTXT: true,
@ -4754,6 +4755,7 @@ func TestSanitize(t *testing.T) {
&net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
&net.UDPAddr{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{ HTTPAddrs: []net.Addr{
&net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678}, &net.TCPAddr{IP: net.ParseIP("1.2.3.4"), Port: 5678},
&net.UnixAddr{Name: "/var/run/foo"}, &net.UnixAddr{Name: "/var/run/foo"},
@ -4894,6 +4896,12 @@ func TestSanitize(t *testing.T) {
"DNSRecursorTimeout": "0s", "DNSRecursorTimeout": "0s",
"DNSRecursors": [], "DNSRecursors": [],
"DNSServiceTTL": {}, "DNSServiceTTL": {},
"DNSSOA": {
"Refresh": 3600,
"Retry": 600,
"Expire": 86400,
"Minttl": 0
},
"DNSUDPAnswerLimit": 0, "DNSUDPAnswerLimit": 0,
"DataDir": "", "DataDir": "",
"Datacenter": "", "Datacenter": "",

View File

@ -940,8 +940,8 @@ func TestCatalog_ListNodes_StaleRead(t *testing.T) {
// Try to join // Try to join
joinLAN(t, s2, s1) joinLAN(t, s2, s1)
testrpc.WaitForLeader(t, s1.RPC, "dc1") testrpc.WaitForTestAgent(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s2.RPC, "dc1") testrpc.WaitForTestAgent(t, s2.RPC, "dc1")
// Use the follower as the client // Use the follower as the client
var codec rpc.ClientCodec var codec rpc.ClientCodec

View File

@ -39,6 +39,13 @@ const (
var InvalidDnsRe = regexp.MustCompile(`[^A-Za-z0-9\\-]+`) 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 { type dnsConfig struct {
AllowStale bool AllowStale bool
Datacenter string Datacenter string
@ -53,6 +60,7 @@ type dnsConfig struct {
UDPAnswerLimit int UDPAnswerLimit int
ARecordLimit int ARecordLimit int
NodeMetaTXT bool NodeMetaTXT bool
dnsSOAConfig dnsSOAConfig
} }
// DNSServer is used to wrap an Agent and expose various // DNSServer is used to wrap an Agent and expose various
@ -97,6 +105,7 @@ func NewDNSServer(a *Agent) (*DNSServer, error) {
return srv, nil return srv, nil
} }
// GetDNSConfig takes global config and creates the config used by DNS server
func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig { func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig {
return &dnsConfig{ return &dnsConfig{
AllowStale: conf.DNSAllowStale, AllowStale: conf.DNSAllowStale,
@ -112,6 +121,12 @@ func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig {
ServiceTTL: conf.DNSServiceTTL, ServiceTTL: conf.DNSServiceTTL,
UDPAnswerLimit: conf.DNSUDPAnswerLimit, UDPAnswerLimit: conf.DNSUDPAnswerLimit,
NodeMetaTXT: conf.DNSNodeMetaTXT, 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, Name: d.domain,
Rrtype: dns.TypeSOA, Rrtype: dns.TypeSOA,
Class: dns.ClassINET, Class: dns.ClassINET,
Ttl: 0, // Has to be consistent with MinTTL to avoid invalidation
Ttl: d.config.dnsSOAConfig.Minttl,
}, },
Ns: "ns." + d.domain, Ns: "ns." + d.domain,
Serial: uint32(time.Now().Unix()), Serial: uint32(time.Now().Unix()),
// todo(fs): make these configurable
Mbox: "hostmaster." + d.domain, Mbox: "hostmaster." + d.domain,
Refresh: 3600, Refresh: d.config.dnsSOAConfig.Refresh,
Retry: 600, Retry: d.config.dnsSOAConfig.Retry,
Expire: 86400, Expire: d.config.dnsSOAConfig.Expire,
Minttl: 0, Minttl: d.config.dnsSOAConfig.Minttl,
} }
} }

View File

@ -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) { func TestDNS_ServiceReverseLookupNodeAddress(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t.Name(), "") a := NewTestAgent(t.Name(), "")

View File

@ -68,7 +68,7 @@ func TestUiNodes(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestAgent(t.Name(), "") a := NewTestAgent(t.Name(), "")
defer a.Shutdown() defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1") testrpc.WaitForTestAgent(t, a.RPC, "dc1")
args := &structs.RegisterRequest{ args := &structs.RegisterRequest{
Datacenter: "dc1", Datacenter: "dc1",

View File

@ -1242,6 +1242,7 @@ func TestAPI_AgentConnectCARoots_list(t *testing.T) {
defer s.Stop() defer s.Stop()
agent := c.Agent() agent := c.Agent()
s.WaitForSerfCheck(t)
list, meta, err := agent.ConnectCARoots(nil) list, meta, err := agent.ConnectCARoots(nil)
require.NoError(err) require.NoError(err)
require.True(meta.LastIndex > 0) require.True(meta.LastIndex > 0)
@ -1286,6 +1287,7 @@ func TestAPI_AgentConnectAuthorize(t *testing.T) {
defer s.Stop() defer s.Stop()
agent := c.Agent() agent := c.Agent()
s.WaitForSerfCheck(t)
params := &AgentAuthorizeParams{ params := &AgentAuthorizeParams{
Target: "foo", Target: "foo",
ClientCertSerial: "fake", ClientCertSerial: "fake",

View File

@ -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 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. defaults to true.
* <a name="soa"></a><a href="#soa">`soa`</a> 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:
* <a name="soa_expire"></a><a href="soa_expire">expire</a> -
Configure SOA Expire duration in seconds, default value is 86400, ie: 24 hours.
* <a name="soa_min_ttl"></a><a href="soa_min_ttl">`min_ttl`</a> -
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.
* <a name="soa_refresh"></a><a href="soa_refresh">refresh</a> -
Configure SOA Refresh duration in seconds, default value is `3600`, ie: 1 hour.
* <a name="soa_retry"></a><a href="soa_retry">retry</a> -
Configures the Retry duration expressed in seconds, default value is
600, ie: 10 minutes.
* <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the * <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the
[`-domain` command-line flag](#_domain). [`-domain` command-line flag](#_domain).

View File

@ -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 "appropriately" simply is turning negative response caching off to get the best
recovery time when a service becomes available again. 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.
<a name="ttl"></a> <a name="ttl"></a>
## TTL Values ## TTL Values