clash/dns/resolver.go
2024-03-22 00:33:38 +08:00

581 lines
14 KiB
Go

package dns
import (
"context"
"errors"
"net/netip"
"strings"
"time"
"github.com/metacubex/mihomo/common/arc"
"github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/component/fakeip"
"github.com/metacubex/mihomo/component/geodata/router"
"github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log"
D "github.com/miekg/dns"
"github.com/samber/lo"
orderedmap "github.com/wk8/go-ordered-map/v2"
"golang.org/x/exp/maps"
"golang.org/x/sync/singleflight"
)
type dnsClient interface {
ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error)
Address() string
}
type dnsCache interface {
GetWithExpire(key string) (*D.Msg, time.Time, bool)
SetWithExpire(key string, value *D.Msg, expire time.Time)
}
type result struct {
Msg *D.Msg
Error error
}
type Resolver struct {
ipv6 bool
ipv6Timeout time.Duration
hosts *trie.DomainTrie[resolver.HostValue]
main []dnsClient
fallback []dnsClient
fallbackDomainFilters []fallbackDomainFilter
fallbackIPFilters []fallbackIPFilter
group singleflight.Group
cache dnsCache
policy []dnsPolicy
proxyServer []dnsClient
}
func (r *Resolver) LookupIPPrimaryIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) {
ch := make(chan []netip.Addr, 1)
go func() {
defer close(ch)
ip, err := r.lookupIP(ctx, host, D.TypeAAAA)
if err != nil {
return
}
ch <- ip
}()
ips, err = r.lookupIP(ctx, host, D.TypeA)
if err == nil {
return
}
ip, open := <-ch
if !open {
return nil, resolver.ErrIPNotFound
}
return ip, nil
}
func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) {
ch := make(chan []netip.Addr, 1)
go func() {
defer close(ch)
ip, err := r.lookupIP(ctx, host, D.TypeAAAA)
if err != nil {
return
}
ch <- ip
}()
ips, err = r.lookupIP(ctx, host, D.TypeA)
var waitIPv6 *time.Timer
if r != nil && r.ipv6Timeout > 0 {
waitIPv6 = time.NewTimer(r.ipv6Timeout)
} else {
waitIPv6 = time.NewTimer(100 * time.Millisecond)
}
defer waitIPv6.Stop()
select {
case ipv6s, open := <-ch:
if !open && err != nil {
return nil, resolver.ErrIPNotFound
}
ips = append(ips, ipv6s...)
case <-waitIPv6.C:
// wait ipv6 result
}
return ips, nil
}
// LookupIPv4 request with TypeA
func (r *Resolver) LookupIPv4(ctx context.Context, host string) ([]netip.Addr, error) {
return r.lookupIP(ctx, host, D.TypeA)
}
// LookupIPv6 request with TypeAAAA
func (r *Resolver) LookupIPv6(ctx context.Context, host string) ([]netip.Addr, error) {
return r.lookupIP(ctx, host, D.TypeAAAA)
}
func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
for _, filter := range r.fallbackIPFilters {
if filter.Match(ip) {
return true
}
}
return false
}
// ExchangeContext a batch of dns request with context.Context, and it use cache
func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 {
return nil, errors.New("should have one question at least")
}
continueFetch := false
defer func() {
if continueFetch || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
go func() {
ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout)
defer cancel()
_, _ = r.exchangeWithoutCache(ctx, m) // ignore result, just for putMsgToCache
}()
}
}()
q := m.Question[0]
cacheM, expireTime, hit := r.cache.GetWithExpire(q.String())
if hit {
log.Debugln("[DNS] cache hit for %s, expire at %s", q.Name, expireTime.Format("2006-01-02 15:04:05"))
now := time.Now()
msg = cacheM.Copy()
if expireTime.Before(now) {
setMsgTTL(msg, uint32(1)) // Continue fetch
continueFetch = true
} else {
// updating TTL by subtracting common delta time from each DNS record
updateMsgTTL(msg, uint32(time.Until(expireTime).Seconds()))
}
return
}
return r.exchangeWithoutCache(ctx, m)
}
// ExchangeWithoutCache a batch of dns request, and it do NOT GET from cache
func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
q := m.Question[0]
retryNum := 0
retryMax := 3
fn := func() (result any, err error) {
ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) // reset timeout in singleflight
defer cancel()
cache := false
defer func() {
if err != nil {
result = retryNum
retryNum++
return
}
msg := result.(*D.Msg)
if cache {
// OPT RRs MUST NOT be cached, forwarded, or stored in or loaded from master files.
msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool {
return rr.Header().Rrtype != D.TypeOPT
})
putMsgToCache(r.cache, q.String(), q, msg)
}
}()
isIPReq := isIPRequest(q)
if isIPReq {
cache = true
return r.ipExchange(ctx, m)
}
if matched := r.matchPolicy(m); len(matched) != 0 {
result, cache, err = batchExchange(ctx, matched, m)
return
}
result, cache, err = batchExchange(ctx, r.main, m)
return
}
ch := r.group.DoChan(q.String(), fn)
var result singleflight.Result
select {
case result = <-ch:
break
case <-ctx.Done():
select {
case result = <-ch: // maybe ctxDone and chFinish in same time, get DoChan's result as much as possible
break
default:
go func() { // start a retrying monitor in background
result := <-ch
ret, err, shared := result.Val, result.Err, result.Shared
if err != nil && !shared && ret.(int) < retryMax { // retry
r.group.DoChan(q.String(), fn)
}
}()
return nil, ctx.Err()
}
}
ret, err, shared := result.Val, result.Err, result.Shared
if err != nil && !shared && ret.(int) < retryMax { // retry
r.group.DoChan(q.String(), fn)
}
if err == nil {
msg = ret.(*D.Msg)
if shared {
msg = msg.Copy()
}
}
return
}
func (r *Resolver) matchPolicy(m *D.Msg) []dnsClient {
if r.policy == nil {
return nil
}
domain := msgToDomain(m)
if domain == "" {
return nil
}
for _, policy := range r.policy {
if dnsClients := policy.Match(domain); len(dnsClients) > 0 {
return dnsClients
}
}
return nil
}
func (r *Resolver) shouldOnlyQueryFallback(m *D.Msg) bool {
if r.fallback == nil || len(r.fallbackDomainFilters) == 0 {
return false
}
domain := msgToDomain(m)
if domain == "" {
return false
}
for _, df := range r.fallbackDomainFilters {
if df.Match(domain) {
return true
}
}
return false
}
func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if matched := r.matchPolicy(m); len(matched) != 0 {
res := <-r.asyncExchange(ctx, matched, m)
return res.Msg, res.Error
}
onlyFallback := r.shouldOnlyQueryFallback(m)
if onlyFallback {
res := <-r.asyncExchange(ctx, r.fallback, m)
return res.Msg, res.Error
}
msgCh := r.asyncExchange(ctx, r.main, m)
if r.fallback == nil || len(r.fallback) == 0 { // directly return if no fallback servers are available
res := <-msgCh
msg, err = res.Msg, res.Error
return
}
res := <-msgCh
if res.Error == nil {
if ips := msgToIP(res.Msg); len(ips) != 0 {
shouldNotFallback := lo.EveryBy(ips, func(ip netip.Addr) bool {
return !r.shouldIPFallback(ip)
})
if shouldNotFallback {
msg, err = res.Msg, res.Error // no need to wait for fallback result
return
}
}
}
res = <-r.asyncExchange(ctx, r.fallback, m)
msg, err = res.Msg, res.Error
return
}
func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) {
ip, err := netip.ParseAddr(host)
if err == nil {
isIPv4 := ip.Is4() || ip.Is4In6()
if dnsType == D.TypeAAAA && !isIPv4 {
return []netip.Addr{ip}, nil
} else if dnsType == D.TypeA && isIPv4 {
return []netip.Addr{ip}, nil
} else {
return []netip.Addr{}, resolver.ErrIPVersion
}
}
query := &D.Msg{}
query.SetQuestion(D.Fqdn(host), dnsType)
msg, err := r.ExchangeContext(ctx, query)
if err != nil {
return []netip.Addr{}, err
}
ips = msgToIP(msg)
ipLength := len(ips)
if ipLength == 0 {
return []netip.Addr{}, resolver.ErrIPNotFound
}
return
}
func (r *Resolver) asyncExchange(ctx context.Context, client []dnsClient, msg *D.Msg) <-chan *result {
ch := make(chan *result, 1)
go func() {
res, _, err := batchExchange(ctx, client, msg)
ch <- &result{Msg: res, Error: err}
}()
return ch
}
// Invalid return this resolver can or can't be used
func (r *Resolver) Invalid() bool {
if r == nil {
return false
}
return len(r.main) > 0
}
type NameServer struct {
Net string
Addr string
Interface string
ProxyAdapter C.ProxyAdapter
ProxyName string
Params map[string]string
PreferH3 bool
}
func (ns NameServer) Equal(ns2 NameServer) bool {
defer func() {
// C.ProxyAdapter compare maybe panic, just ignore
recover()
}()
if ns.Net == ns2.Net &&
ns.Addr == ns2.Addr &&
ns.Interface == ns2.Interface &&
ns.ProxyAdapter == ns2.ProxyAdapter &&
ns.ProxyName == ns2.ProxyName &&
maps.Equal(ns.Params, ns2.Params) &&
ns.PreferH3 == ns2.PreferH3 {
return true
}
return false
}
type FallbackFilter struct {
GeoIP bool
GeoIPCode string
IPCIDR []netip.Prefix
Domain []string
GeoSite []router.DomainMatcher
}
type Config struct {
Main, Fallback []NameServer
Default []NameServer
ProxyServer []NameServer
IPv6 bool
IPv6Timeout uint
EnhancedMode C.DNSMode
FallbackFilter FallbackFilter
Pool *fakeip.Pool
Hosts *trie.DomainTrie[resolver.HostValue]
Policy *orderedmap.OrderedMap[string, []NameServer]
RuleProviders map[string]provider.RuleProvider
CacheAlgorithm string
}
func NewResolver(config Config) *Resolver {
var cache dnsCache
if config.CacheAlgorithm == "lru" {
cache = lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true))
} else {
cache = arc.New(arc.WithSize[string, *D.Msg](4096))
}
defaultResolver := &Resolver{
main: transform(config.Default, nil),
cache: cache,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
var nameServerCache []struct {
NameServer
dnsClient
}
cacheTransform := func(nameserver []NameServer) (result []dnsClient) {
LOOP:
for _, ns := range nameserver {
for _, nsc := range nameServerCache {
if nsc.NameServer.Equal(ns) {
result = append(result, nsc.dnsClient)
continue LOOP
}
}
// not in cache
dc := transform([]NameServer{ns}, defaultResolver)
if len(dc) > 0 {
dc := dc[0]
nameServerCache = append(nameServerCache, struct {
NameServer
dnsClient
}{NameServer: ns, dnsClient: dc})
result = append(result, dc)
}
}
return
}
if config.CacheAlgorithm == "" || config.CacheAlgorithm == "lru" {
cache = lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true))
} else {
cache = arc.New(arc.WithSize[string, *D.Msg](4096))
}
r := &Resolver{
ipv6: config.IPv6,
main: cacheTransform(config.Main),
cache: cache,
hosts: config.Hosts,
ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond,
}
if len(config.Fallback) != 0 {
r.fallback = cacheTransform(config.Fallback)
}
if len(config.ProxyServer) != 0 {
r.proxyServer = cacheTransform(config.ProxyServer)
}
if config.Policy.Len() != 0 {
r.policy = make([]dnsPolicy, 0)
var triePolicy *trie.DomainTrie[[]dnsClient]
insertPolicy := func(policy dnsPolicy) {
if triePolicy != nil {
triePolicy.Optimize()
r.policy = append(r.policy, domainTriePolicy{triePolicy})
triePolicy = nil
}
if policy != nil {
r.policy = append(r.policy, policy)
}
}
for pair := config.Policy.Oldest(); pair != nil; pair = pair.Next() {
domain, nameserver := pair.Key, pair.Value
if temp := strings.Split(domain, ":"); len(temp) == 2 {
prefix := temp[0]
key := temp[1]
switch prefix {
case "rule-set":
if p, ok := config.RuleProviders[key]; ok {
log.Debugln("Adding rule-set policy: %s ", key)
insertPolicy(domainSetPolicy{
domainSetProvider: p,
dnsClients: cacheTransform(nameserver),
})
continue
} else {
log.Warnln("Can't found ruleset policy: %s", key)
}
case "geosite":
inverse := false
if strings.HasPrefix(key, "!") {
inverse = true
key = key[1:]
}
log.Debugln("Adding geosite policy: %s inversed %t", key, inverse)
matcher, err := NewGeoSite(key)
if err != nil {
log.Warnln("adding geosite policy %s error: %s", key, err)
continue
}
insertPolicy(geositePolicy{
matcher: matcher,
inverse: inverse,
dnsClients: cacheTransform(nameserver),
})
continue // skip triePolicy new
}
}
if triePolicy == nil {
triePolicy = trie.New[[]dnsClient]()
}
_ = triePolicy.Insert(domain, cacheTransform(nameserver))
}
insertPolicy(nil)
}
fallbackIPFilters := []fallbackIPFilter{}
if config.FallbackFilter.GeoIP {
fallbackIPFilters = append(fallbackIPFilters, &geoipFilter{
code: config.FallbackFilter.GeoIPCode,
})
}
for _, ipnet := range config.FallbackFilter.IPCIDR {
fallbackIPFilters = append(fallbackIPFilters, &ipnetFilter{ipnet: ipnet})
}
r.fallbackIPFilters = fallbackIPFilters
fallbackDomainFilters := []fallbackDomainFilter{}
if len(config.FallbackFilter.Domain) != 0 {
fallbackDomainFilters = append(fallbackDomainFilters, NewDomainFilter(config.FallbackFilter.Domain))
}
if len(config.FallbackFilter.GeoSite) != 0 {
fallbackDomainFilters = append(fallbackDomainFilters, &geoSiteFilter{
matchers: config.FallbackFilter.GeoSite,
})
}
r.fallbackDomainFilters = fallbackDomainFilters
return r
}
func NewProxyServerHostResolver(old *Resolver) *Resolver {
r := &Resolver{
ipv6: old.ipv6,
main: old.proxyServer,
cache: old.cache,
hosts: old.hosts,
ipv6Timeout: old.ipv6Timeout,
}
return r
}
var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go