plugin/forward: configurable domain support for healthcheck (#5281)

* plugin/forward: configurable domain support for healthcheck

Signed-off-by: hansedong <admin@yinxiaoluo.com>
This commit is contained in:
hansedong 2022-04-13 00:39:48 +08:00 committed by GitHub
parent e60c179194
commit 0622a6c66c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 35 deletions

View file

@ -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... {
* `<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`.
* `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

View file

@ -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

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 {
@ -223,16 +225,21 @@ func TestSetupHealthCheck(t *testing.T) {
tests := []struct {
input string
shouldErr bool
expectedVal 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)
}
}
}