2023-03-24 12:55:51 +00:00
|
|
|
package proxy
|
2018-02-05 22:00:47 +00:00
|
|
|
|
|
|
|
|
import (
|
2026-06-15 02:54:05 +02:00
|
|
|
"context"
|
2018-07-09 15:14:55 +01:00
|
|
|
"crypto/tls"
|
2026-09-08 12:51:18 +08:00
|
|
|
"errors"
|
2026-07-10 02:38:22 +02:00
|
|
|
"net"
|
2026-06-15 02:54:05 +02:00
|
|
|
"net/http"
|
2018-02-05 22:00:47 +00:00
|
|
|
"sync/atomic"
|
2018-07-09 15:14:55 +01:00
|
|
|
"time"
|
2018-02-05 22:00:47 +00:00
|
|
|
|
2026-06-15 02:54:05 +02:00
|
|
|
"github.com/coredns/coredns/plugin/pkg/doh"
|
2023-03-24 12:55:51 +00:00
|
|
|
"github.com/coredns/coredns/plugin/pkg/log"
|
2018-09-19 07:29:37 +01:00
|
|
|
"github.com/coredns/coredns/plugin/pkg/transport"
|
|
|
|
|
|
2018-02-05 22:00:47 +00:00
|
|
|
"github.com/miekg/dns"
|
|
|
|
|
)
|
|
|
|
|
|
2018-07-09 15:14:55 +01:00
|
|
|
// HealthChecker checks the upstream health.
|
|
|
|
|
type HealthChecker interface {
|
|
|
|
|
Check(*Proxy) error
|
|
|
|
|
SetTLSConfig(*tls.Config)
|
2023-03-24 12:55:51 +00:00
|
|
|
GetTLSConfig() *tls.Config
|
2020-03-06 11:52:43 +01:00
|
|
|
SetRecursionDesired(bool)
|
|
|
|
|
GetRecursionDesired() bool
|
2022-04-13 00:39:48 +08:00
|
|
|
SetDomain(domain string)
|
|
|
|
|
GetDomain() string
|
2022-02-09 15:45:52 +01:00
|
|
|
SetTCPTransport()
|
2023-03-24 12:55:51 +00:00
|
|
|
GetReadTimeout() time.Duration
|
|
|
|
|
SetReadTimeout(time.Duration)
|
|
|
|
|
GetWriteTimeout() time.Duration
|
|
|
|
|
SetWriteTimeout(time.Duration)
|
2026-07-10 02:38:22 +02:00
|
|
|
SetLocalAddress(net.IP)
|
|
|
|
|
GetLocalAddress() net.IP
|
2018-07-09 15:14:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dnsHc is a health checker for a DNS endpoint (DNS, and DoT).
|
2020-03-06 11:52:43 +01:00
|
|
|
type dnsHc struct {
|
|
|
|
|
c *dns.Client
|
|
|
|
|
recursionDesired bool
|
2022-04-13 00:39:48 +08:00
|
|
|
domain string
|
2023-07-04 15:35:55 +01:00
|
|
|
|
|
|
|
|
proxyName string
|
2026-07-10 02:38:22 +02:00
|
|
|
|
|
|
|
|
localAddress net.IP
|
2020-03-06 11:52:43 +01:00
|
|
|
}
|
2018-07-09 15:14:55 +01:00
|
|
|
|
2026-06-15 02:54:05 +02:00
|
|
|
const defaultTimeout = 1 * time.Second
|
|
|
|
|
|
2018-09-19 07:29:37 +01:00
|
|
|
// NewHealthChecker returns a new HealthChecker based on transport.
|
2026-06-15 02:54:05 +02:00
|
|
|
func NewHealthChecker(proxyName, protocol string, recursionDesired bool, domain string) HealthChecker {
|
|
|
|
|
switch protocol {
|
2018-09-19 07:29:37 +01:00
|
|
|
case transport.DNS, transport.TLS:
|
2018-07-09 15:14:55 +01:00
|
|
|
c := new(dns.Client)
|
|
|
|
|
c.Net = "udp"
|
plugin/pkg/proxy: set IP_RECVERR on upstream UDP sockets (#8628)
Without IP_RECVERR, Linux passes only some ICMP errors, such as port
unreachable, to a connected UDP socket. It drops net and host unreachable
(net/ipv4/udp.c, __udp4_lib_err), so a query to an upstream behind an
unreachable route waits out the read timeout before forward tries the
next upstream, and health checks wait out theirs before marking it down.
Set IP_RECVERR (IPV6_RECVERR for IPv6) on the UDP sockets for upstream
queries and health checks, so the read fails when the ICMP error arrives.
TCP sockets are left alone, and nothing changes outside Linux. The health
check client gets an explicit 2s dial timeout, since a Dialer replaces
dns.Client's default one. Because the Dialer now exists from the start,
setDialer no longer creates a bare one when source_address is set, which
also lost the dial timeout before.
Fixes #8621
Signed-off-by: MohibShaikh <mohibuddin9@gmail.com>
2026-10-13 12:45:06 +05:00
|
|
|
// A Dialer replaces dns.Client's own 2s dial timeout, so keep it.
|
|
|
|
|
c.Dialer = &net.Dialer{Timeout: 2 * time.Second, Control: recvErr}
|
2026-07-10 02:38:22 +02:00
|
|
|
setDefaultTimeout(c)
|
2018-07-09 15:14:55 +01:00
|
|
|
|
2023-03-24 12:55:51 +00:00
|
|
|
return &dnsHc{
|
|
|
|
|
c: c,
|
|
|
|
|
recursionDesired: recursionDesired,
|
|
|
|
|
domain: domain,
|
2023-07-04 15:35:55 +01:00
|
|
|
proxyName: proxyName,
|
2023-03-24 12:55:51 +00:00
|
|
|
}
|
2026-06-15 02:54:05 +02:00
|
|
|
case transport.HTTPS:
|
|
|
|
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
|
|
|
|
httpTransport.TLSClientConfig = new(tls.Config)
|
|
|
|
|
|
|
|
|
|
return &dohHc{
|
|
|
|
|
client: &http.Client{
|
|
|
|
|
Transport: httpTransport,
|
|
|
|
|
Timeout: defaultTimeout,
|
|
|
|
|
},
|
|
|
|
|
recursionDesired: recursionDesired,
|
|
|
|
|
domain: domain,
|
|
|
|
|
proxyName: proxyName,
|
|
|
|
|
}
|
2026-09-08 12:51:18 +08:00
|
|
|
case transport.QUIC:
|
|
|
|
|
return &doqHc{
|
|
|
|
|
recursionDesired: recursionDesired,
|
|
|
|
|
domain: domain,
|
|
|
|
|
proxyName: proxyName,
|
|
|
|
|
readTimeout: defaultTimeout,
|
|
|
|
|
writeTimeout: defaultTimeout,
|
|
|
|
|
}
|
2018-07-09 15:14:55 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:54:05 +02:00
|
|
|
log.Warningf("No healthchecker for transport %q", protocol)
|
2018-07-09 15:14:55 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 12:51:18 +08:00
|
|
|
// doqHc is a health checker for a DNS-over-QUIC endpoint. It uses the same
|
|
|
|
|
// reusable QUIC connection as normal forwarded queries.
|
|
|
|
|
type doqHc struct {
|
|
|
|
|
tlsConfig *tls.Config
|
|
|
|
|
recursionDesired bool
|
|
|
|
|
domain string
|
|
|
|
|
proxyName string
|
|
|
|
|
localAddress net.IP
|
|
|
|
|
readTimeout time.Duration
|
|
|
|
|
writeTimeout time.Duration
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *doqHc) Check(p *Proxy) error {
|
|
|
|
|
if p.doq == nil {
|
|
|
|
|
return errors.New("proxy: DoQ transport is not initialized")
|
|
|
|
|
}
|
|
|
|
|
ping := new(dns.Msg)
|
|
|
|
|
ping.SetQuestion(h.domain, dns.TypeNS)
|
|
|
|
|
ping.RecursionDesired = h.recursionDesired
|
|
|
|
|
|
|
|
|
|
timeout := max(h.readTimeout, h.writeTimeout)
|
|
|
|
|
if timeout <= 0 {
|
|
|
|
|
timeout = defaultTimeout
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
for range 2 {
|
|
|
|
|
_, _, err = p.doq.exchange(ctx, ping, timeout)
|
|
|
|
|
if !errors.Is(err, ErrCachedClosed) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
healthcheckFailureCount.WithLabelValues(p.proxyName, p.addr).Inc()
|
|
|
|
|
p.incrementFails()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
atomic.StoreUint32(&p.fails, 0)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *doqHc) SetTLSConfig(cfg *tls.Config) { h.tlsConfig = cfg }
|
|
|
|
|
func (h *doqHc) GetTLSConfig() *tls.Config { return h.tlsConfig }
|
|
|
|
|
func (h *doqHc) SetRecursionDesired(v bool) { h.recursionDesired = v }
|
|
|
|
|
func (h *doqHc) GetRecursionDesired() bool { return h.recursionDesired }
|
|
|
|
|
func (h *doqHc) SetDomain(domain string) { h.domain = domain }
|
|
|
|
|
func (h *doqHc) GetDomain() string { return h.domain }
|
|
|
|
|
func (h *doqHc) SetTCPTransport() {}
|
|
|
|
|
func (h *doqHc) GetReadTimeout() time.Duration {
|
|
|
|
|
return h.readTimeout
|
|
|
|
|
}
|
|
|
|
|
func (h *doqHc) SetReadTimeout(timeout time.Duration) {
|
|
|
|
|
h.readTimeout = timeout
|
|
|
|
|
}
|
|
|
|
|
func (h *doqHc) GetWriteTimeout() time.Duration {
|
|
|
|
|
return h.writeTimeout
|
|
|
|
|
}
|
|
|
|
|
func (h *doqHc) SetWriteTimeout(timeout time.Duration) {
|
|
|
|
|
h.writeTimeout = timeout
|
|
|
|
|
}
|
|
|
|
|
func (h *doqHc) SetLocalAddress(addr net.IP) {
|
|
|
|
|
h.localAddress = append(net.IP(nil), addr...)
|
|
|
|
|
}
|
|
|
|
|
func (h *doqHc) GetLocalAddress() net.IP {
|
|
|
|
|
return append(net.IP(nil), h.localAddress...)
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-09 15:14:55 +01:00
|
|
|
func (h *dnsHc) SetTLSConfig(cfg *tls.Config) {
|
|
|
|
|
h.c.Net = "tcp-tls"
|
|
|
|
|
h.c.TLSConfig = cfg
|
2026-07-10 02:38:22 +02:00
|
|
|
// update the dialer accordingly with the protocol changed
|
|
|
|
|
h.setDialer()
|
2018-07-09 15:14:55 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:55:51 +00:00
|
|
|
func (h *dnsHc) GetTLSConfig() *tls.Config {
|
|
|
|
|
return h.c.TLSConfig
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-06 11:52:43 +01:00
|
|
|
func (h *dnsHc) SetRecursionDesired(recursionDesired bool) {
|
|
|
|
|
h.recursionDesired = recursionDesired
|
|
|
|
|
}
|
|
|
|
|
func (h *dnsHc) GetRecursionDesired() bool {
|
|
|
|
|
return h.recursionDesired
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-13 00:39:48 +08:00
|
|
|
func (h *dnsHc) SetDomain(domain string) {
|
|
|
|
|
h.domain = domain
|
|
|
|
|
}
|
|
|
|
|
func (h *dnsHc) GetDomain() string {
|
|
|
|
|
return h.domain
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-09 15:45:52 +01:00
|
|
|
func (h *dnsHc) SetTCPTransport() {
|
|
|
|
|
h.c.Net = "tcp"
|
2026-07-10 02:38:22 +02:00
|
|
|
// update the dialer accordingly with the protocol changed
|
|
|
|
|
h.setDialer()
|
2022-02-09 15:45:52 +01:00
|
|
|
}
|
|
|
|
|
|
2023-03-24 12:55:51 +00:00
|
|
|
func (h *dnsHc) GetReadTimeout() time.Duration {
|
|
|
|
|
return h.c.ReadTimeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dnsHc) SetReadTimeout(t time.Duration) {
|
|
|
|
|
h.c.ReadTimeout = t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dnsHc) GetWriteTimeout() time.Duration {
|
|
|
|
|
return h.c.WriteTimeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dnsHc) SetWriteTimeout(t time.Duration) {
|
|
|
|
|
h.c.WriteTimeout = t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For HC, we send to . IN NS +[no]rec message to the upstream. Dial timeouts and empty
|
2018-02-05 22:00:47 +00:00
|
|
|
// replies are considered fails, basically anything else constitutes a healthy upstream.
|
|
|
|
|
|
2018-02-15 10:21:57 +01:00
|
|
|
// Check is used as the up.Func in the up.Probe.
|
2018-07-09 15:14:55 +01:00
|
|
|
func (h *dnsHc) Check(p *Proxy) error {
|
|
|
|
|
err := h.send(p.addr)
|
2018-02-05 22:00:47 +00:00
|
|
|
if err != nil {
|
2023-07-04 15:35:55 +01:00
|
|
|
healthcheckFailureCount.WithLabelValues(p.proxyName, p.addr).Add(1)
|
2023-04-16 22:08:56 +08:00
|
|
|
p.incrementFails()
|
2018-02-15 10:21:57 +01:00
|
|
|
return err
|
2018-02-05 22:00:47 +00:00
|
|
|
}
|
|
|
|
|
|
2018-02-15 10:21:57 +01:00
|
|
|
atomic.StoreUint32(&p.fails, 0)
|
|
|
|
|
return nil
|
2018-02-05 22:00:47 +00:00
|
|
|
}
|
|
|
|
|
|
2018-07-09 15:14:55 +01:00
|
|
|
func (h *dnsHc) send(addr string) error {
|
|
|
|
|
ping := new(dns.Msg)
|
2022-04-13 00:39:48 +08:00
|
|
|
ping.SetQuestion(h.domain, dns.TypeNS)
|
2025-04-04 20:27:39 +02:00
|
|
|
ping.RecursionDesired = h.recursionDesired
|
2018-02-05 22:00:47 +00:00
|
|
|
|
2018-07-09 15:14:55 +01:00
|
|
|
m, _, err := h.c.Exchange(ping, addr)
|
|
|
|
|
// If we got a header, we're alright, basically only care about I/O errors 'n stuff.
|
2018-02-05 22:00:47 +00:00
|
|
|
if err != nil && m != nil {
|
2018-07-09 15:14:55 +01:00
|
|
|
// Silly check, something sane came back.
|
2018-02-05 22:00:47 +00:00
|
|
|
if m.Response || m.Opcode == dns.OpcodeQuery {
|
|
|
|
|
err = nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-06-15 02:54:05 +02:00
|
|
|
|
2026-07-10 02:38:22 +02:00
|
|
|
// SetLocalAddress sets the local address in transport.
|
|
|
|
|
func (h *dnsHc) SetLocalAddress(localAddr net.IP) {
|
|
|
|
|
h.localAddress = localAddr
|
|
|
|
|
h.setDialer()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetLocalAddress returns the local address in transport.
|
|
|
|
|
func (h *dnsHc) GetLocalAddress() net.IP {
|
|
|
|
|
return h.localAddress
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setDialer sets the local address in the underlying dialer
|
|
|
|
|
func (h *dnsHc) setDialer() {
|
|
|
|
|
if h.localAddress == nil {
|
|
|
|
|
if h.c.Dialer != nil {
|
|
|
|
|
h.c.Dialer.LocalAddr = nil
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if h.c.Dialer == nil {
|
|
|
|
|
h.c.Dialer = new(net.Dialer)
|
|
|
|
|
setDefaultTimeout(h.c)
|
|
|
|
|
}
|
|
|
|
|
if h.c.Net == "udp" {
|
|
|
|
|
h.c.Dialer.LocalAddr = &net.UDPAddr{IP: h.localAddress}
|
|
|
|
|
} else {
|
|
|
|
|
h.c.Dialer.LocalAddr = &net.TCPAddr{IP: h.localAddress}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setDefaultTimeout sets the default read and write timeout values for the DNS client to 1 second.
|
|
|
|
|
func setDefaultTimeout(c *dns.Client) {
|
|
|
|
|
c.ReadTimeout = 1 * time.Second
|
|
|
|
|
c.WriteTimeout = 1 * time.Second
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 02:54:05 +02:00
|
|
|
// dohHc is a health checker for a DNS-over-HTTPS (DoH) endpoint.
|
|
|
|
|
type dohHc struct {
|
|
|
|
|
client *http.Client
|
|
|
|
|
recursionDesired bool
|
|
|
|
|
domain string
|
|
|
|
|
proxyName string
|
2026-07-10 02:38:22 +02:00
|
|
|
localAddress net.IP
|
2026-06-15 02:54:05 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) Check(p *Proxy) error {
|
2026-07-22 19:07:09 -07:00
|
|
|
err := h.send(p.addr, p.dohHost)
|
2026-06-15 02:54:05 +02:00
|
|
|
if err != nil {
|
|
|
|
|
healthcheckFailureCount.WithLabelValues(p.proxyName, p.addr).Add(1)
|
|
|
|
|
p.incrementFails()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
atomic.StoreUint32(&p.fails, 0)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 19:07:09 -07:00
|
|
|
func (h *dohHc) send(addr, host string) error {
|
2026-06-15 02:54:05 +02:00
|
|
|
ping := new(dns.Msg)
|
|
|
|
|
ping.SetQuestion(h.domain, dns.TypeNS)
|
|
|
|
|
ping.RecursionDesired = h.recursionDesired
|
|
|
|
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), h.client.Timeout)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
2026-07-22 19:07:09 -07:00
|
|
|
req, err := doh.NewRequestWithContext(ctx, http.MethodPost, addr, host, ping)
|
2026-06-15 02:54:05 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := h.client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ResponseToMsg always closes the body via defer resp.Body.Close().
|
|
|
|
|
m, err := doh.ResponseToMsg(resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we got a header, we're alright.
|
|
|
|
|
if m.Response || m.Opcode == dns.OpcodeQuery {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetTLSConfig(cfg *tls.Config) {
|
|
|
|
|
h.client.Transport.(*http.Transport).TLSClientConfig = cfg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) GetTLSConfig() *tls.Config {
|
|
|
|
|
return h.client.Transport.(*http.Transport).TLSClientConfig
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetRecursionDesired(recursionDesired bool) {
|
|
|
|
|
h.recursionDesired = recursionDesired
|
|
|
|
|
}
|
|
|
|
|
func (h *dohHc) GetRecursionDesired() bool {
|
|
|
|
|
return h.recursionDesired
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetDomain(domain string) {
|
|
|
|
|
h.domain = domain
|
|
|
|
|
}
|
|
|
|
|
func (h *dohHc) GetDomain() string {
|
|
|
|
|
return h.domain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetTCPTransport() {
|
|
|
|
|
// no-op for DoH
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) GetReadTimeout() time.Duration {
|
|
|
|
|
return h.client.Transport.(*http.Transport).ResponseHeaderTimeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetReadTimeout(t time.Duration) {
|
|
|
|
|
h.client.Transport.(*http.Transport).ResponseHeaderTimeout = t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) GetWriteTimeout() time.Duration {
|
|
|
|
|
return h.client.Timeout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) SetWriteTimeout(t time.Duration) {
|
|
|
|
|
h.client.Timeout = t
|
|
|
|
|
}
|
2026-07-10 02:38:22 +02:00
|
|
|
|
|
|
|
|
func (h *dohHc) SetLocalAddress(localAddr net.IP) {
|
|
|
|
|
h.localAddress = localAddr
|
|
|
|
|
httpTransport := h.client.Transport.(*http.Transport)
|
|
|
|
|
if localAddr == nil {
|
|
|
|
|
httpTransport.DialContext = nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
dialer := &net.Dialer{LocalAddr: &net.TCPAddr{IP: localAddr}}
|
|
|
|
|
httpTransport.DialContext = dialer.DialContext
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) GetLocalAddress() net.IP {
|
|
|
|
|
return h.localAddress
|
|
|
|
|
}
|