diff --git a/plugin/forward/README.md b/plugin/forward/README.md index 892701f70..1ea335bf2 100644 --- a/plugin/forward/README.md +++ b/plugin/forward/README.md @@ -50,7 +50,7 @@ forward FROM TO... { tls CERT KEY CA tls_servername NAME policy random|round_robin|sequential - health_check DURATION [no_rec] + health_check DURATION [no_rec] [domain DOMAIN] max_concurrent MAX } ~~~ @@ -90,6 +90,8 @@ forward FROM TO... { * `` - 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`. + * `domain DOMAIN` - optional arguments that sets the domain of the dns-query used in health checking. + If not configured, the requested domain name is `.`. `DOMAIN` is used to configure the domain name. * `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 REFUSED response. This response does not count as a health failure. When choosing a value for **MAX**, pick a number @@ -182,6 +184,18 @@ service with health checks. } ~~~ +Or configure other domain name for health check requests + +~~~ corefile +. { + forward . tls://9.9.9.9 { + tls_servername dns.quad9.net + health_check 5s domain example.org + } + cache 30 +} +~~~ + Or with multiple upstreams from the same provider ~~~ corefile diff --git a/plugin/forward/forward.go b/plugin/forward/forward.go index 19a469c72..e7fcaaad6 100644 --- a/plugin/forward/forward.go +++ b/plugin/forward/forward.go @@ -56,7 +56,7 @@ type Forward struct { // New returns a new Forward. func New() *Forward { - f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(random), from: ".", hcInterval: hcInterval, opts: options{forceTCP: false, preferUDP: false, hcRecursionDesired: true}} + f := &Forward{maxfails: 2, tlsConfig: new(tls.Config), expire: defaultExpire, p: new(random), from: ".", hcInterval: hcInterval, opts: options{forceTCP: false, preferUDP: false, hcRecursionDesired: true, hcDomain: "."}} return f } @@ -234,6 +234,7 @@ type options struct { forceTCP bool preferUDP bool hcRecursionDesired bool + hcDomain string } var defaultTimeout = 5 * time.Second diff --git a/plugin/forward/health.go b/plugin/forward/health.go index f4cfab834..ec0b48143 100644 --- a/plugin/forward/health.go +++ b/plugin/forward/health.go @@ -16,6 +16,8 @@ type HealthChecker interface { SetTLSConfig(*tls.Config) SetRecursionDesired(bool) GetRecursionDesired() bool + SetDomain(domain string) + GetDomain() string SetTCPTransport() } @@ -23,6 +25,7 @@ type HealthChecker interface { type dnsHc struct { c *dns.Client recursionDesired bool + domain string } var ( @@ -31,7 +34,7 @@ var ( ) // NewHealthChecker returns a new HealthChecker based on transport. -func NewHealthChecker(trans string, recursionDesired bool) HealthChecker { +func NewHealthChecker(trans string, recursionDesired bool, domain string) HealthChecker { switch trans { case transport.DNS, transport.TLS: c := new(dns.Client) @@ -39,7 +42,7 @@ func NewHealthChecker(trans string, recursionDesired bool) HealthChecker { c.ReadTimeout = hcReadTimeout c.WriteTimeout = hcWriteTimeout - return &dnsHc{c: c, recursionDesired: recursionDesired} + return &dnsHc{c: c, recursionDesired: recursionDesired, domain: domain} } log.Warningf("No healthchecker for transport %q", trans) @@ -58,6 +61,13 @@ func (h *dnsHc) GetRecursionDesired() bool { return h.recursionDesired } +func (h *dnsHc) SetDomain(domain string) { + h.domain = domain +} +func (h *dnsHc) GetDomain() string { + return h.domain +} + func (h *dnsHc) SetTCPTransport() { h.c.Net = "tcp" } @@ -80,7 +90,7 @@ func (h *dnsHc) Check(p *Proxy) error { func (h *dnsHc) send(addr string) error { ping := new(dns.Msg) - ping.SetQuestion(".", dns.TypeNS) + ping.SetQuestion(h.domain, dns.TypeNS) ping.MsgHdr.RecursionDesired = h.recursionDesired m, _, err := h.c.Exchange(ping, addr) diff --git a/plugin/forward/health_test.go b/plugin/forward/health_test.go index 2d65f4353..9917b3a37 100644 --- a/plugin/forward/health_test.go +++ b/plugin/forward/health_test.go @@ -242,3 +242,42 @@ func TestHealthNoMaxFails(t *testing.T) { t.Errorf("Expected number of health checks to be %d, got %d", 0, i1) } } + +func TestHealthDomain(t *testing.T) { + hcReadTimeout = 10 * time.Millisecond + readTimeout = 10 * time.Millisecond + defaultTimeout = 10 * time.Millisecond + hcWriteTimeout = 10 * time.Millisecond + hcDomain := "example.org." + 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 == hcDomain && r.RecursionDesired == true { + 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.SetDomain(hcDomain) + f := New() + f.SetProxy(p) + defer f.OnShutdown() + + req := new(dns.Msg) + req.SetQuestion(".", dns.TypeNS) + + f.ServeDNS(context.TODO(), &test.ResponseWriter{}, req) + + time.Sleep(20 * time.Millisecond) + i1 := atomic.LoadUint32(&i) + if i1 != 1 { + t.Errorf("Expected number of health checks with Domain==%s to be %d, got %d", hcDomain, 1, i1) + } +} diff --git a/plugin/forward/proxy.go b/plugin/forward/proxy.go index 80cb581f9..6a4b5693e 100644 --- a/plugin/forward/proxy.go +++ b/plugin/forward/proxy.go @@ -29,7 +29,7 @@ func NewProxy(addr, trans string) *Proxy { probe: up.New(), transport: newTransport(addr), } - p.health = NewHealthChecker(trans, true) + p.health = NewHealthChecker(trans, true, ".") runtime.SetFinalizer(p, (*Proxy).finalizer) return p } diff --git a/plugin/forward/setup.go b/plugin/forward/setup.go index baf0512ba..af8141898 100644 --- a/plugin/forward/setup.go +++ b/plugin/forward/setup.go @@ -151,6 +151,7 @@ func parseStanza(c *caddy.Controller) (*Forward, error) { if f.opts.forceTCP && transports[i] != transport.TLS { f.proxies[i].health.SetTCPTransport() } + f.proxies[i].health.SetDomain(f.opts.hcDomain) } return f, nil @@ -187,11 +188,17 @@ func parseBlock(c *caddy.Controller, f *Forward) error { return fmt.Errorf("health_check can't be negative: %d", dur) } f.hcInterval = dur + f.opts.hcDomain = "." for c.NextArg() { switch hcOpts := c.Val(); hcOpts { case "no_rec": f.opts.hcRecursionDesired = false + case "domain": + if !c.NextArg() { + return c.ArgErr() + } + f.opts.hcDomain = c.Val() default: return fmt.Errorf("health_check: unknown option %s", hcOpts) } diff --git a/plugin/forward/setup_test.go b/plugin/forward/setup_test.go index 3c38acf97..2a5257e98 100644 --- a/plugin/forward/setup_test.go +++ b/plugin/forward/setup_test.go @@ -20,25 +20,27 @@ func TestSetup(t *testing.T) { expectedErr string }{ // positive - {"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{hcRecursionDesired: true}, ""}, - {"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, hcRecursionDesired: 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, hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true}, ""}, - {"forward 10.9.3.0/18 127.0.0.1", false, "0.9.10.in-addr.arpa.", nil, 2, options{hcRecursionDesired: true}, ""}, + {"forward . 127.0.0.1", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "example.org"}, ""}, + {"forward . 127.0.0.1 {\nexcept miek.nl\n}\n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nmax_fails 3\n}\n", false, ".", nil, 3, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\n}\n", false, ".", nil, 2, options{forceTCP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 {\nforce_tcp\nprefer_udp\n}\n", false, ".", nil, 2, options{preferUDP: true, forceTCP: true, hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1:8080", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . [::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . [2003::1]:53", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward . 127.0.0.1 \n", false, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, + {"forward 10.9.3.0/18 127.0.0.1", false, "0.9.10.in-addr.arpa.", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, ""}, // negative - {"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{hcRecursionDesired: true}, "unknown property"}, + {"forward . a27.0.0.1", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "not an IP"}, + {"forward . 127.0.0.1 {\nblaatl\n}\n", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "unknown property"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "Wrong argument count or unexpected line ending after 'domain'"}, {`forward . ::1 - forward com ::2`, true, "", nil, 0, options{hcRecursionDesired: true}, "plugin"}, - {"forward . https://127.0.0.1 \n", true, ".", nil, 2, options{hcRecursionDesired: true}, "'https' is not supported as a destination protocol in forward: https://127.0.0.1"}, - {"forward xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127.0.0.1 \n", true, ".", nil, 2, options{hcRecursionDesired: true}, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"}, + forward com ::2`, true, "", nil, 0, options{hcRecursionDesired: true, hcDomain: "."}, "plugin"}, + {"forward . https://127.0.0.1 \n", true, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, "'https' is not supported as a destination protocol in forward: https://127.0.0.1"}, + {"forward xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 127.0.0.1 \n", true, ".", nil, 2, options{hcRecursionDesired: true, hcDomain: "."}, "unable to normalize 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'"}, } for i, test := range tests { @@ -221,18 +223,23 @@ func TestSetupMaxConcurrent(t *testing.T) { func TestSetupHealthCheck(t *testing.T) { tests := []struct { - input string - shouldErr bool - expectedVal bool - expectedErr string + input string + shouldErr bool + expectedRecVal bool + expectedDomain string + 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, ""}, + {"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, ".", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s no_rec domain example.org\n}\n", false, false, "example.org", ""}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain example.org\n}\n", false, true, "example.org", ""}, // 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"}, + {"forward . 127.0.0.1 {\nhealth_check no_rec\n}\n", true, true, ".", "time: invalid duration"}, + {"forward . 127.0.0.1 {\nhealth_check domain example.org\n}\n", true, true, "example.org", "time: invalid duration"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s rec\n}\n", true, true, ".", "health_check: unknown option rec"}, + {"forward . 127.0.0.1 {\nhealth_check 0.5s domain\n}\n", true, true, ".", "Wrong argument count or unexpected line ending after 'domain'"}, } for i, test := range tests { @@ -247,13 +254,13 @@ func TestSetupHealthCheck(t *testing.T) { 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) + if !test.shouldErr && (f.opts.hcRecursionDesired != test.expectedRecVal || f.proxies[0].health.GetRecursionDesired() != test.expectedRecVal || + f.opts.hcDomain != test.expectedDomain || f.proxies[0].health.GetDomain() != test.expectedDomain) { + t.Errorf("Test %d: expectedRec: %v, got: %v. expectedDomain: %s, got: %s. ", i, test.expectedRecVal, f.opts.hcRecursionDesired, test.expectedDomain, f.opts.hcDomain) } } }