2022-11-07 10:21:24 +03:00
|
|
|
package agdnet
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"math"
|
|
|
|
"net"
|
2023-09-06 08:22:07 +03:00
|
|
|
"net/netip"
|
2022-11-07 10:21:24 +03:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
|
|
"github.com/AdguardTeam/golibs/log"
|
|
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Resolver is the DNS resolver interface.
|
|
|
|
//
|
2024-01-04 19:22:32 +03:00
|
|
|
// See [net.Resolver].
|
|
|
|
//
|
|
|
|
// TODO(a.garipov): Move to golibs and unite with the one in module dnsproxy.
|
2022-11-07 10:21:24 +03:00
|
|
|
type Resolver interface {
|
2023-09-06 08:22:07 +03:00
|
|
|
// LookupNetIP returns a slice of host's IP addresses of family specified by
|
|
|
|
// fam, which must be either [netutil.AddrFamilyIPv4] or
|
|
|
|
// [netutil.AddrFamilyIPv6].
|
|
|
|
LookupNetIP(
|
|
|
|
ctx context.Context,
|
|
|
|
fam netutil.AddrFamily,
|
|
|
|
host string,
|
|
|
|
) (ips []netip.Addr, err error)
|
2022-11-07 10:21:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// DefaultResolver uses [net.DefaultResolver] to resolve addresses.
|
|
|
|
type DefaultResolver struct{}
|
|
|
|
|
|
|
|
// type check
|
|
|
|
var _ Resolver = DefaultResolver{}
|
|
|
|
|
2023-09-06 08:22:07 +03:00
|
|
|
// LookupNetIP implements the [Resolver] interface for DefaultResolver.
|
|
|
|
func (DefaultResolver) LookupNetIP(
|
2022-11-07 10:21:24 +03:00
|
|
|
ctx context.Context,
|
|
|
|
fam netutil.AddrFamily,
|
|
|
|
host string,
|
2023-09-06 08:22:07 +03:00
|
|
|
) (ips []netip.Addr, err error) {
|
2022-11-07 10:21:24 +03:00
|
|
|
switch fam {
|
|
|
|
case netutil.AddrFamilyIPv4:
|
2023-09-06 08:22:07 +03:00
|
|
|
return net.DefaultResolver.LookupNetIP(ctx, "ip4", host)
|
2022-11-07 10:21:24 +03:00
|
|
|
case netutil.AddrFamilyIPv6:
|
2023-09-06 08:22:07 +03:00
|
|
|
return net.DefaultResolver.LookupNetIP(ctx, "ip6", host)
|
2022-11-07 10:21:24 +03:00
|
|
|
default:
|
|
|
|
return nil, net.UnknownNetworkError(fam.String())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// resolveCache is a simple address resolving cache.
|
|
|
|
type resolveCache map[string]*resolveCacheItem
|
|
|
|
|
|
|
|
// resolveCacheItem is an item of [resolveCache].
|
|
|
|
type resolveCacheItem struct {
|
|
|
|
refrTime time.Time
|
2023-09-06 08:22:07 +03:00
|
|
|
ips []netip.Addr
|
2022-11-07 10:21:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// CachingResolver caches resolved results for hosts for a certain time,
|
|
|
|
// regardless of the actual TTLs of the records. It is used for caching the
|
|
|
|
// results of lookups of hostnames that don't change their IP addresses often.
|
|
|
|
type CachingResolver struct {
|
|
|
|
resolver Resolver
|
|
|
|
|
|
|
|
// mu protects ip4 and ip6.
|
|
|
|
mu *sync.Mutex
|
|
|
|
ipv4 resolveCache
|
|
|
|
ipv6 resolveCache
|
|
|
|
|
|
|
|
ttl time.Duration
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewCachingResolver returns a new caching resolver.
|
|
|
|
func NewCachingResolver(resolver Resolver, ttl time.Duration) (c *CachingResolver) {
|
|
|
|
return &CachingResolver{
|
|
|
|
resolver: resolver,
|
|
|
|
|
|
|
|
mu: &sync.Mutex{},
|
|
|
|
ipv4: resolveCache{},
|
|
|
|
ipv6: resolveCache{},
|
|
|
|
|
|
|
|
ttl: ttl,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// type check
|
|
|
|
var _ Resolver = (*CachingResolver)(nil)
|
|
|
|
|
2023-09-06 08:22:07 +03:00
|
|
|
// LookupNetIP implements the [Resolver] interface for *CachingResolver. host
|
2022-11-07 10:21:24 +03:00
|
|
|
// should be normalized. Slice ips and its elements must not be mutated.
|
2023-09-06 08:22:07 +03:00
|
|
|
func (c *CachingResolver) LookupNetIP(
|
2022-11-07 10:21:24 +03:00
|
|
|
ctx context.Context,
|
|
|
|
fam netutil.AddrFamily,
|
|
|
|
host string,
|
2023-09-06 08:22:07 +03:00
|
|
|
) (ips []netip.Addr, err error) {
|
2022-11-07 10:21:24 +03:00
|
|
|
c.mu.Lock()
|
|
|
|
defer c.mu.Unlock()
|
|
|
|
|
|
|
|
var item *resolveCacheItem
|
|
|
|
switch fam {
|
|
|
|
case netutil.AddrFamilyIPv4:
|
|
|
|
item = c.ipv4[host]
|
|
|
|
case netutil.AddrFamilyIPv6:
|
|
|
|
item = c.ipv6[host]
|
|
|
|
default:
|
|
|
|
return nil, net.UnknownNetworkError(fam.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
if item == nil || time.Since(item.refrTime) > c.ttl {
|
|
|
|
item, err = c.resolve(ctx, fam, host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return item.ips, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// resolve looks up the IP addresses for host and puts them into the cache.
|
|
|
|
func (c *CachingResolver) resolve(
|
|
|
|
ctx context.Context,
|
|
|
|
fam netutil.AddrFamily,
|
|
|
|
host string,
|
|
|
|
) (item *resolveCacheItem, err error) {
|
2023-09-06 08:22:07 +03:00
|
|
|
var ips []netip.Addr
|
2022-11-07 10:21:24 +03:00
|
|
|
|
|
|
|
refrTime := time.Now()
|
|
|
|
|
|
|
|
// Don't resolve IP addresses.
|
2023-08-08 18:31:48 +03:00
|
|
|
ip := ipFromHost(host, fam)
|
2023-09-06 08:22:07 +03:00
|
|
|
if ip != (netip.Addr{}) {
|
|
|
|
ips = []netip.Addr{ip}
|
2022-11-07 10:21:24 +03:00
|
|
|
|
|
|
|
// Set the refresh time to the maximum date that time.Duration allows to
|
|
|
|
// prevent this item from refreshing.
|
|
|
|
refrTime = time.Unix(0, math.MaxInt64)
|
|
|
|
} else {
|
2023-09-06 08:22:07 +03:00
|
|
|
ips, err = c.resolver.LookupNetIP(ctx, fam, host)
|
2022-11-07 10:21:24 +03:00
|
|
|
if err != nil {
|
|
|
|
if !isExpectedLookupError(fam, err) {
|
|
|
|
return nil, fmt.Errorf("resolving %s addr for %q: %w", fam, host, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug("caching resolver: warning: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var cache resolveCache
|
|
|
|
if fam == netutil.AddrFamilyIPv4 {
|
|
|
|
cache = c.ipv4
|
|
|
|
} else {
|
|
|
|
cache = c.ipv6
|
|
|
|
}
|
|
|
|
|
|
|
|
item = &resolveCacheItem{
|
|
|
|
refrTime: refrTime,
|
|
|
|
ips: ips,
|
|
|
|
}
|
|
|
|
|
|
|
|
cache[host] = item
|
|
|
|
|
|
|
|
return item, nil
|
|
|
|
}
|
|
|
|
|
2023-09-06 08:22:07 +03:00
|
|
|
// ipFromHost parses host as if it'd be an IP address of specified fam. It
|
|
|
|
// returns an empty netip.
|
|
|
|
func ipFromHost(host string, fam netutil.AddrFamily) (ip netip.Addr) {
|
|
|
|
var famFunc func(netip.Addr) (ok bool)
|
|
|
|
switch fam {
|
|
|
|
case netutil.AddrFamilyIPv4:
|
|
|
|
famFunc = netip.Addr.Is4
|
|
|
|
case netutil.AddrFamilyIPv6:
|
|
|
|
famFunc = netip.Addr.Is6
|
|
|
|
default:
|
|
|
|
return netip.Addr{}
|
2023-08-08 18:31:48 +03:00
|
|
|
}
|
|
|
|
|
2023-09-06 08:22:07 +03:00
|
|
|
ip, err := netip.ParseAddr(host)
|
|
|
|
if err != nil || !famFunc(ip) {
|
|
|
|
return netip.Addr{}
|
2023-08-08 18:31:48 +03:00
|
|
|
}
|
|
|
|
|
2023-09-06 08:22:07 +03:00
|
|
|
return ip
|
2023-08-08 18:31:48 +03:00
|
|
|
}
|
|
|
|
|
2022-11-07 10:21:24 +03:00
|
|
|
// isExpectedLookupError returns true if the error is an expected lookup error.
|
|
|
|
func isExpectedLookupError(fam netutil.AddrFamily, err error) (ok bool) {
|
|
|
|
var dnsErr *net.DNSError
|
|
|
|
if fam == netutil.AddrFamilyIPv6 && errors.As(err, &dnsErr) {
|
|
|
|
// It's expected that Go default DNS resolver returns a DNS error in
|
|
|
|
// some cases when it receives an empty response. It's unclear what
|
|
|
|
// exactly triggers this error, though.
|
|
|
|
//
|
|
|
|
// TODO(ameshkov): Consider researching this in detail.
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
var addrErr *net.AddrError
|
|
|
|
if !errors.As(err, &addrErr) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expect the error about no suitable addresses. For example, no IPv6
|
|
|
|
// addresses for a host that does have IPv4 ones.
|
|
|
|
//
|
|
|
|
// See function filterAddrList in ${GOROOT}/src/net/ipsock.go.
|
|
|
|
return addrErr.Err == "no suitable address found"
|
|
|
|
}
|