mirror of
https://github.com/hanzoai/traces.git
synced 2026-11-02 12:18:48 +00:00
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>
442 lines
13 KiB
Go
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)
|
|
}
|