Commit 116bda4d authored by Christian Tryti's avatar Christian Tryti Committed by GitHub

Add configuration flag to set if RecursionDesired should be set on health...

Add configuration flag to set if RecursionDesired should be set on health checkers in Forward-plugin (#3679)

* Make the RD-flag in health-checks in the Forward-plugin configurable

Introduces a new configuration flag; `health_check_non_recursive`. This
flag makes the health-checker do non-recursive requests when checking
the health of upstream servers.
Signed-off-by: default avatarGeir Haugom <ghagit@haugom.org>
Signed-off-by: default avatarChristian Tryti <ctryti@gmail.com>

* Changes after feedback from reviewer

* Better tests of health-checks with and without recursion
* Removed the health_check_non_recursive configuration in favor of
extending the existing health_check configuration. Now supports an
optional `no_rec` argument.
Signed-off-by: default avatarChristian Tryti <ctryti@gmail.com>

* Add new test that checks setup of health_check.
Signed-off-by: default avatarChristian Tryti <ctryti@gmail.com>
parent a74a2091
...@@ -51,7 +51,7 @@ forward FROM TO... { ...@@ -51,7 +51,7 @@ forward FROM TO... {
tls CERT KEY CA tls CERT KEY CA
tls_servername NAME tls_servername NAME
policy random|round_robin|sequential policy random|round_robin|sequential
health_check DURATION health_check DURATION [no_rec]
max_concurrent MAX max_concurrent MAX
} }
~~~ ~~~
...@@ -85,7 +85,10 @@ forward FROM TO... { ...@@ -85,7 +85,10 @@ forward FROM TO... {
* `random` is a policy that implements random upstream selection. * `random` is a policy that implements random upstream selection.
* `round_robin` is a policy that selects hosts based on round robin ordering. * `round_robin` is a policy that selects hosts based on round robin ordering.
* `sequential` is a policy that selects hosts based on sequential ordering. * `sequential` is a policy that selects hosts based on sequential ordering.
* `health_check`, use a different **DURATION** for health checking, the default duration is 0.5s. * `health_check` configure the behaviour of health checking of the upstream servers
* `<duration>` - use a different duration for health checking, the default duration is 0.5s.
* `no_rec` - optional argument that sets the RecursionDesired-flag of the dns-query used in health checking to `false`.
The flag is default `true`.
* `max_concurrent` **MAX** will limit the number of concurrent queries to **MAX**. Any new query that would * `max_concurrent` **MAX** will limit the number of concurrent queries to **MAX**. Any new query that would
raise the number of concurrent queries above the **MAX** will result in a SERVFAIL response. This raise the number of concurrent queries above the **MAX** will result in a SERVFAIL response. This
response does not count as a health failure. When choosing a value for **MAX**, pick a number response does not count as a health failure. When choosing a value for **MAX**, pick a number
......
...@@ -52,7 +52,7 @@ type Forward struct { ...@@ -52,7 +52,7 @@ type Forward struct {
// New returns a new Forward. // New returns a new Forward.
func New() *Forward { func New() *Forward {
f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(policy.Random), from: ".", hcInterval: hcInterval} f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(policy.Random), from: ".", hcInterval: hcInterval, opts: options{forceTCP: false, preferUDP: false, hcRecursionDesired: true}}
return f return f
} }
...@@ -224,8 +224,9 @@ var ( ...@@ -224,8 +224,9 @@ var (
// options holds various options that can be set. // options holds various options that can be set.
type options struct { type options struct {
forceTCP bool forceTCP bool
preferUDP bool preferUDP bool
hcRecursionDesired bool
} }
const defaultTimeout = 5 * time.Second const defaultTimeout = 5 * time.Second
...@@ -14,13 +14,18 @@ import ( ...@@ -14,13 +14,18 @@ import (
type HealthChecker interface { type HealthChecker interface {
Check(*Proxy) error Check(*Proxy) error
SetTLSConfig(*tls.Config) SetTLSConfig(*tls.Config)
SetRecursionDesired(bool)
GetRecursionDesired() bool
} }
// dnsHc is a health checker for a DNS endpoint (DNS, and DoT). // dnsHc is a health checker for a DNS endpoint (DNS, and DoT).
type dnsHc struct{ c *dns.Client } type dnsHc struct {
c *dns.Client
recursionDesired bool
}
// NewHealthChecker returns a new HealthChecker based on transport. // NewHealthChecker returns a new HealthChecker based on transport.
func NewHealthChecker(trans string) HealthChecker { func NewHealthChecker(trans string, recursionDesired bool) HealthChecker {
switch trans { switch trans {
case transport.DNS, transport.TLS: case transport.DNS, transport.TLS:
c := new(dns.Client) c := new(dns.Client)
...@@ -28,7 +33,7 @@ func NewHealthChecker(trans string) HealthChecker { ...@@ -28,7 +33,7 @@ func NewHealthChecker(trans string) HealthChecker {
c.ReadTimeout = 1 * time.Second c.ReadTimeout = 1 * time.Second
c.WriteTimeout = 1 * time.Second c.WriteTimeout = 1 * time.Second
return &dnsHc{c: c} return &dnsHc{c: c, recursionDesired: recursionDesired}
} }
log.Warningf("No healthchecker for transport %q", trans) log.Warningf("No healthchecker for transport %q", trans)
...@@ -40,7 +45,14 @@ func (h *dnsHc) SetTLSConfig(cfg *tls.Config) { ...@@ -40,7 +45,14 @@ func (h *dnsHc) SetTLSConfig(cfg *tls.Config) {
h.c.TLSConfig = cfg h.c.TLSConfig = cfg
} }
// For HC we send to . IN NS +norec message to the upstream. Dial timeouts and empty func (h *dnsHc) SetRecursionDesired(recursionDesired bool) {
h.recursionDesired = recursionDesired
}
func (h *dnsHc) GetRecursionDesired() bool {
return h.recursionDesired
}
// For HC we send to . IN NS +[no]rec message to the upstream. Dial timeouts and empty
// replies are considered fails, basically anything else constitutes a healthy upstream. // replies are considered fails, basically anything else constitutes a healthy upstream.
// Check is used as the up.Func in the up.Probe. // Check is used as the up.Func in the up.Probe.
...@@ -59,6 +71,7 @@ func (h *dnsHc) Check(p *Proxy) error { ...@@ -59,6 +71,7 @@ func (h *dnsHc) Check(p *Proxy) error {
func (h *dnsHc) send(addr string) error { func (h *dnsHc) send(addr string) error {
ping := new(dns.Msg) ping := new(dns.Msg)
ping.SetQuestion(".", dns.TypeNS) ping.SetQuestion(".", dns.TypeNS)
ping.MsgHdr.RecursionDesired = h.recursionDesired
m, _, err := h.c.Exchange(ping, addr) m, _, err := h.c.Exchange(ping, addr)
// If we got a header, we're alright, basically only care about I/O errors 'n stuff. // If we got a header, we're alright, basically only care about I/O errors 'n stuff.
......
...@@ -14,10 +14,15 @@ import ( ...@@ -14,10 +14,15 @@ import (
) )
func TestHealth(t *testing.T) { func TestHealth(t *testing.T) {
const expected = 0 const expected = 1
i := uint32(0) i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) { s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if r.Question[0].Name == "." { if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking
atomic.AddUint32(&q, 1)
return
}
if r.Question[0].Name == "." && r.RecursionDesired == true {
atomic.AddUint32(&i, 1) atomic.AddUint32(&i, 1)
} }
ret := new(dns.Msg) ret := new(dns.Msg)
...@@ -39,7 +44,43 @@ func TestHealth(t *testing.T) { ...@@ -39,7 +44,43 @@ func TestHealth(t *testing.T) {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i) i1 := atomic.LoadUint32(&i)
if i1 != expected { if i1 != expected {
t.Errorf("Expected number of health checks to be %d, got %d", expected, i1) t.Errorf("Expected number of health checks with RecursionDesired==true to be %d, got %d", expected, i1)
}
}
func TestHealthNoRecursion(t *testing.T) {
const expected = 1
i := uint32(0)
q := uint32(0)
s := dnstest.NewServer(func(w dns.ResponseWriter, r *dns.Msg) {
if atomic.LoadUint32(&q) == 0 { //drop the first query to trigger health-checking
atomic.AddUint32(&q, 1)
return
}
if r.Question[0].Name == "." && r.RecursionDesired == false {
atomic.AddUint32(&i, 1)
}
ret := new(dns.Msg)
ret.SetReply(r)
w.WriteMsg(ret)
})
defer s.Close()
p := NewProxy(s.Addr, transport.DNS)
p.health.SetRecursionDesired(false)
f := New()
f.SetProxy(p)
defer f.OnShutdown()
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req)
time.Sleep(1 * time.Second)
i1 := atomic.LoadUint32(&i)
if i1 != expected {
t.Errorf("Expected number of health checks with RecursionDesired==false to be %d, got %d", expected, i1)
} }
} }
......
...@@ -29,7 +29,7 @@ func NewProxy(addr, trans string) *Proxy { ...@@ -29,7 +29,7 @@ func NewProxy(addr, trans string) *Proxy {
probe: up.New(), probe: up.New(),
transport: newTransport(addr), transport: newTransport(addr),
} }
p.health = NewHealthChecker(trans) p.health = NewHealthChecker(trans, true)
runtime.SetFinalizer(p, (*Proxy).finalizer) runtime.SetFinalizer(p, (*Proxy).finalizer)
return p return p
} }
......
...@@ -121,6 +121,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) { ...@@ -121,6 +121,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) {
f.proxies[i].SetTLSConfig(f.tlsConfig) f.proxies[i].SetTLSConfig(f.tlsConfig)
} }
f.proxies[i].SetExpire(f.expire) f.proxies[i].SetExpire(f.expire)
f.proxies[i].health.SetRecursionDesired(f.opts.hcRecursionDesired)
} }
return f, nil return f, nil
...@@ -161,6 +162,16 @@ func parseBlock(c *caddy.Controller, f *Forward) error { ...@@ -161,6 +162,16 @@ func parseBlock(c *caddy.Controller, f *Forward) error {
return fmt.Errorf("health_check can't be negative: %d", dur) return fmt.Errorf("health_check can't be negative: %d", dur)
} }
f.hcInterval = dur f.hcInterval = dur
for c.NextArg() {
switch hcOpts := c.Val(); hcOpts {
case "no_rec":
f.opts.hcRecursionDesired = false
default:
return fmt.Errorf("health_check: unknown option %s", hcOpts)
}
}
case "force_tcp": case "force_tcp":
if c.NextArg() { if c.NextArg() {
return c.ArgErr() return c.ArgErr()
......
...@@ -21,21 +21,22 @@ func TestSetup(t *testing.T) { ...@@ -21,21 +21,22 @@ func TestSetup(t *testing.T) {
expectedErr string expectedErr string
}{ }{
// positive // positive
{"forward . 127.0.0.1", false, ".", nil, 2, options{}, ""}, {"forward . 127.0.0.1", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{}, ""}, {"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{}, ""}, {"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true}, ""}, {"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true}, ""}, {"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true}, ""}, {"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true, hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1:53", false, ".", nil, 2, options{}, ""}, {"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1:8080", false, ".", nil, 2, options{}, ""}, {"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . [::1]:53", false, ".", nil, 2, options{}, ""}, {"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . [2003::1]:53", false, ".", nil, 2, options{}, ""}, {"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
{"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""},
// negative // negative
{"forward . a27.0.0.1", true, "", nil, 0, options{}, "not an IP"}, {"forward . a27.0.0.1", true, "", nil, 0, options{hcRecursionDesired: true}, "not an IP"},
{"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{}, "unknown property"}, {"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{hcRecursionDesired: true}, "unknown property"},
{`forward . ::1 {`forward . ::1
forward com ::2`, true, "", nil, 0, options{}, "plugin"}, forward com ::2`, true, "", nil, 0, options{hcRecursionDesired: true}, "plugin"},
} }
for i, test := range tests { for i, test := range tests {
...@@ -210,3 +211,42 @@ func TestSetupMaxConcurrent(t *testing.T) { ...@@ -210,3 +211,42 @@ func TestSetupMaxConcurrent(t *testing.T) {
} }
} }
} }
func TestSetupHealthCheck(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal bool
expectedErr string
}{
// positive
{"forward . 127.0.0.1\n", false, true, ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s\n}\n", false, true, ""},
{"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec\n}\n", false, false, ""},
// negative
{"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, "time: invalid duration"},
{"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, "health_check: unknown option rec"},
}
for i, test := range tests {
c := caddy.NewTestController("dns", test.input)
f, err := parseForward(c)
if test.shouldErr && err == nil {
t.Errorf("Test %d: expected error but found %s for input %s", i, err, test.input)
}
if err != nil {
if !test.shouldErr {
t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.input, err)
}
if !strings.Contains(err.Error(), test.expectedErr) {
t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.input)
}
}
if !test.shouldErr && (f.opts.hcRecursionDesired != test.expectedVal || f.proxies[0].health.GetRecursionDesired() != test.expectedVal) {
t.Errorf("Test %d: expected: %t, got: %d", i, test.expectedVal, f.maxConcurrent)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment