Files
renovate-sh-app[bot]GitHubrenovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
03147cb9f1 fix(deps): update github.com/grafana/dskit digest to a6f1538 (#5860)
Signed-off-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
Co-authored-by: renovate-sh-app[bot] <219655108+renovate-sh-app[bot]@users.noreply.github.com>
2025-11-07 14:54:32 +00:00

442 lines
13 KiB
Go

// Provenance-includes-location: https://github.com/thanos-io/thanos/blob/main/pkg/discovery/dns/miekgdns/resolver.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Thanos Authors.
package miekgdns
import (
"context"
"fmt"
"net"
"slices"
"sync"
"time"
"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/miekg/dns"
"github.com/grafana/dskit/multierror"
)
const (
DefaultResolvConfPath = "/etc/resolv.conf"
defaultResolvConfReload = time.Second * 5
defaultMaxConnsPerHost = 2
)
type Client interface {
Exchange(ctx context.Context, msg *dns.Msg, server string) (*dns.Msg, time.Duration, error)
Clean(known []string)
}
// Resolver is a DNS service discovery backend that retries on errors, only uses TCP,
// and pools connections to nameservers to increase reliability.
//
// This backend:
// * Does _not_ use search domains, all names are assumed to be fully qualified
// * Only uses TCP connections to the nameservers
// * Keeps several connections to each nameserver open
// * Reads resolv.conf periodically, using stale configuration if this fails
// * Closes connections to unknown nameservers periodically
//
// The following parts of resolv.conf are supported:
// * `nameserver` setting
// * `attempts` option
//
// The following parts of resolv.conf are NOT supported.
// * `search` setting
// * `timeout` option
// * `ndots` option
type Resolver struct {
client Client
logger log.Logger
confPath string
reloadPeriod time.Duration
stop chan struct{}
mtx sync.RWMutex
conf *dns.ClientConfig
}
// NewResolver creates a new Resolver that uses the provided resolv.conf configuration
// to perform DNS queries. Configuration from resolv.conf will be periodically reloaded.
func NewResolver(resolvConf string, logger log.Logger) *Resolver {
return NewResolverWithClient(resolvConf, logger, defaultResolvConfReload, NewPoolingClient(defaultMaxConnsPerHost))
}
// NewResolverWithClient creates a new Resolver that uses the provided resolv.conf configuration,
// reload period, and Client implementation to perform DNS queries. Configuration from resolv.conf
// will be periodically reloaded.
func NewResolverWithClient(resolvConf string, logger log.Logger, reloadPeriod time.Duration, client Client) *Resolver {
r := &Resolver{
client: client,
logger: logger,
confPath: resolvConf,
reloadPeriod: reloadPeriod,
stop: make(chan struct{}),
}
// Attempt an initial load of the configuration but fallback to defaults if it fails. The file
// missing should not be fatal according to `man 5 resolv.conf`.
if err := r.loadConfig(); err != nil {
level.Warn(r.logger).Log("msg", "unable to load resolv.conf, using default values", "path", r.confPath, "err", err)
r.conf = defaultClientConfig()
}
// Attempt to reload configuration periodically. If the reloads fail, old values are used.
go r.loop()
return r
}
// Stop stops periodic tasks run by the Resolver. The resolver should not be used after it is stopped.
func (r *Resolver) Stop() {
close(r.stop)
}
func (r *Resolver) IsNotFound(error) bool {
// We don't return an error when there are no hosts found so this is
// always false. Instead, we return the empty DNS response with the
// appropriate return code set.
return false
}
func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) {
return r.lookupSRV(ctx, r.getConfig(), service, proto, name)
}
func (r *Resolver) lookupSRV(ctx context.Context, conf *dns.ClientConfig, service, proto, name string) (cname string, addrs []*net.SRV, err error) {
var target string
if service == "" && proto == "" {
target = name
} else {
target = "_" + service + "._" + proto + "." + name
}
response, err := r.query(ctx, conf, target, dns.Type(dns.TypeSRV))
if err != nil {
return "", nil, err
}
for _, record := range response.Answer {
switch addr := record.(type) {
case *dns.SRV:
addrs = append(addrs, &net.SRV{
Weight: addr.Weight,
Target: addr.Target,
Priority: addr.Priority,
Port: addr.Port,
})
default:
// Ignore unexpected non-SRV responses. This matches the behavior of the
// built-in go resolver. See https://github.com/grafana/mimir/issues/12713
level.Debug(r.logger).Log(
"msg", "unexpected non-SRV response record",
"target", target,
"record", record,
)
}
}
return "", addrs, err
}
func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) {
return r.lookupIPAddr(ctx, r.getConfig(), host, 1, 8)
}
func (r *Resolver) lookupIPAddr(ctx context.Context, conf *dns.ClientConfig, host string, currIteration, maxIterations int) ([]net.IPAddr, error) {
// We want to protect from infinite loops when resolving DNS records recursively.
if currIteration > maxIterations {
return nil, fmt.Errorf("maximum number of recursive iterations reached (%d)", maxIterations)
}
response, err := r.query(ctx, conf, host, dns.Type(dns.TypeAAAA))
if err != nil || len(response.Answer) == 0 {
// Ugly fallback to A lookup.
response, err = r.query(ctx, conf, host, dns.Type(dns.TypeA))
if err != nil {
return nil, err
}
}
var resp []net.IPAddr
for _, record := range response.Answer {
switch addr := record.(type) {
case *dns.A:
resp = append(resp, net.IPAddr{IP: addr.A})
case *dns.AAAA:
resp = append(resp, net.IPAddr{IP: addr.AAAA})
case *dns.CNAME:
// Recursively resolve it.
addrs, err := r.lookupIPAddr(ctx, conf, addr.Target, currIteration+1, maxIterations)
if err != nil {
return nil, fmt.Errorf("%w: recursively resolve %s", err, addr.Target)
}
resp = append(resp, addrs...)
default:
return nil, fmt.Errorf("invalid A, AAAA or CNAME response record %s", record)
}
}
return resp, nil
}
func (r *Resolver) query(ctx context.Context, conf *dns.ClientConfig, name string, qType dns.Type) (*dns.Msg, error) {
// We don't support search domains, all names are assumed to be fully qualified already.
msg := new(dns.Msg).SetQuestion(dns.Fqdn(name), uint16(qType))
merr := multierror.New()
// `man 5 resolv.conf` says that we should try each server, continuing to the next if
// there is a timeout. We should repeat this process up to "attempt" times trying to get
// a viable response.
//
// > (The algorithm used is to try a name server, and if the query times out, try the next,
// > until out of name servers, then repeat trying all the name servers until a maximum number
// > of retries are made.)
for i := 0; i < conf.Attempts; i++ {
for _, ip := range conf.Servers {
server := net.JoinHostPort(ip, conf.Port)
response, _, err := r.client.Exchange(ctx, msg, server)
if err != nil {
merr.Add(fmt.Errorf("resolution against server %s: %w", server, err))
continue
}
if response.Truncated {
merr.Add(fmt.Errorf("resolution against server %s: response truncated", server))
continue
}
if response.Rcode == dns.RcodeSuccess || response.Rcode == dns.RcodeNameError {
return response, nil
}
}
}
return nil, fmt.Errorf("could not resolve %s: no servers returned a viable answer. Errs %s", name, merr.Err())
}
// loop periodically reloads configuration from resolv.conf until the resolver is stopped.
func (r *Resolver) loop() {
ticker := time.NewTicker(r.reloadPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := r.loadConfig(); err != nil {
level.Warn(r.logger).Log("msg", "unable to reload resolv.conf, using old values", "path", r.confPath, "err", err)
}
case <-r.stop:
return
}
}
}
// loadConfig loads and updates configuration from the configured resolv.conf path,
// closing connections to defunct servers that are no longer configured, and returning
// an error if the configuration file couldn't be opened or parsed. If an error is
// returned, connections are not closed and the stored configuration is not updated.
func (r *Resolver) loadConfig() error {
conf, err := dns.ClientConfigFromFile(r.confPath)
if err != nil {
return fmt.Errorf("could not load %s: %w", r.confPath, err)
}
// Note that we're building the list of known servers for the r.client.Clean method
// below from the newly read configuration before updating r.conf so that we know
// that the configuration can't be modified by another thread.
servers := make([]string, len(conf.Servers))
for i, ip := range conf.Servers {
servers[i] = net.JoinHostPort(ip, conf.Port)
}
r.mtx.Lock()
r.conf = conf
r.mtx.Unlock()
// Close connections to any servers that are no longer in resolv.conf.
r.client.Clean(servers)
return nil
}
func (r *Resolver) getConfig() *dns.ClientConfig {
r.mtx.RLock()
defer r.mtx.RUnlock()
return &dns.ClientConfig{
Servers: r.conf.Servers,
Search: r.conf.Search,
Port: r.conf.Port,
Ndots: r.conf.Ndots,
Timeout: r.conf.Timeout,
Attempts: r.conf.Attempts,
}
}
// defaultClientConfig returns Default values if resolv.conf can't be loaded, picked based on values from `man 5 resolv.conf`
func defaultClientConfig() *dns.ClientConfig {
return &dns.ClientConfig{
Servers: []string{"127.0.0.1"},
Search: []string{},
Port: "53",
Ndots: 1,
Timeout: 5,
Attempts: 2,
}
}
// PoolingClient is a DNS client that pools TCP connections to each nameserver.
type PoolingClient struct {
network string
maxOpen int
mtx sync.Mutex
pools map[string]*Pool
}
// NewPoolingClient creates a new PoolingClient instance that keeps up to maxOpen connections to each nameserver.
func NewPoolingClient(maxOpen int) *PoolingClient {
return &PoolingClient{
network: "tcp",
maxOpen: maxOpen,
pools: make(map[string]*Pool),
}
}
// Exchange sends the DNS msg to the nameserver using a pooled connection. The nameserver must be
// of the form "ip:port".
func (c *PoolingClient) Exchange(ctx context.Context, msg *dns.Msg, server string) (*dns.Msg, time.Duration, error) {
pool := c.getPool(server)
conn, err := pool.Get(ctx, c.network, server)
if err != nil {
return nil, 0, fmt.Errorf("unable to create connection to %s via %s: %w", server, c.network, err)
}
connOk := true
defer func() {
if connOk {
_ = pool.Put(conn)
} else {
pool.Discard(conn)
}
}()
client := &dns.Client{Net: c.network}
response, rtt, err := client.ExchangeWithConnContext(ctx, msg, conn)
if err != nil {
connOk = false
}
return response, rtt, err
}
// Clean closes connections to any nameservers that are _not_ part of list of known
// nameservers. The nameservers must be of the form "ip:port", the same format as the
// Exchange method.
func (c *PoolingClient) Clean(known []string) {
c.mtx.Lock()
defer c.mtx.Unlock()
for server, pool := range c.pools {
if slices.Contains(known, server) {
continue
}
pool.Close()
delete(c.pools, server)
}
}
func (c *PoolingClient) getPool(server string) *Pool {
c.mtx.Lock()
defer c.mtx.Unlock()
p, ok := c.pools[server]
if !ok {
p = NewPool(c.maxOpen)
c.pools[server] = p
}
return p
}
// Pool is a pool of DNS connections for a single DNS server.
type Pool struct {
mtx sync.RWMutex
conns chan *dns.Conn
closed bool
}
// NewPool creates a new DNS connection Pool, keeping up to maxConns open.
func NewPool(maxConns int) *Pool {
return &Pool{
conns: make(chan *dns.Conn, maxConns),
}
}
// Get gets an existing connection from the pool or creates a new one if there are no
// pooled connections available. If the pool has been closed, an error is returned.
func (p *Pool) Get(ctx context.Context, network string, server string) (*dns.Conn, error) {
p.mtx.RLock()
defer p.mtx.RUnlock()
if p.closed {
return nil, fmt.Errorf("connection pool for %s %s is closed", network, server)
}
select {
case conn := <-p.conns:
return conn, nil
default:
return p.newConn(ctx, network, server)
}
}
// Put returns a healthy connection to the pool, potentially closing it if the pool is
// already at capacity. If the pool has been closed, the connection will be closed immediately.
func (p *Pool) Put(conn *dns.Conn) error {
p.mtx.RLock()
defer p.mtx.RUnlock()
if p.closed {
return conn.Close()
}
select {
case p.conns <- conn:
return nil
default:
return conn.Close()
}
}
// Discard closes and does not return the given broken connection to the pool.
func (p *Pool) Discard(conn *dns.Conn) {
_ = conn.Close()
}
// Close shuts down this pool, closing all existing connections and preventing new connections
// from being created. Any attempts to get a connection from this pool after it is closed will
// result in an error.
func (p *Pool) Close() {
p.mtx.Lock()
defer p.mtx.Unlock()
p.closed = true
for {
select {
case c := <-p.conns:
_ = c.Close()
default:
return
}
}
}
func (p *Pool) newConn(ctx context.Context, network string, server string) (*dns.Conn, error) {
client := &dns.Client{Net: network}
return client.DialContext(ctx, server)
}