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-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"
|
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,
|
|
|
|
|
}
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
err := h.send(p.addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
healthcheckFailureCount.WithLabelValues(p.proxyName, p.addr).Add(1)
|
|
|
|
|
p.incrementFails()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
atomic.StoreUint32(&p.fails, 0)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *dohHc) send(addr string) error {
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
req, err := doh.NewRequestWithContext(ctx, http.MethodPost, addr, ping)
|
|
|
|
|
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
|
|
|
|
|
}
|