package dns import ( "context" "errors" "net/netip" "strings" "time" "github.com/Dreamacro/clash/common/atomic" "github.com/Dreamacro/clash/common/cache" "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/component/geodata/router" "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/component/trie" C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/constant/provider" "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" "github.com/samber/lo" "golang.org/x/sync/singleflight" ) type dnsClient interface { ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) Address() string } type result struct { Msg *D.Msg Error error } type geositePolicyRecord struct { matcher fallbackDomainFilter policy *Policy inversedMatching bool } type domainSetPolicyRecord struct { domainSetProvider provider.RuleProvider policy *Policy } type Resolver struct { ipv6 bool ipv6Timeout time.Duration hosts *trie.DomainTrie[resolver.HostValue] main []dnsClient fallback []dnsClient fallbackDomainFilters []fallbackDomainFilter fallbackIPFilters []fallbackIPFilter group singleflight.Group lruCache *cache.LruCache[string, *D.Msg] policy *trie.DomainTrie[*Policy] domainSetPolicy []domainSetPolicyRecord geositePolicy []geositePolicyRecord 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.lruCache.GetWithExpire(q.String()) if hit { 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.lruCache, 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 } record := r.policy.Search(domain) if record != nil { p := record.Data() return p.GetData() } for _, geositeRecord := range r.geositePolicy { matched := geositeRecord.matcher.Match(domain) if matched != geositeRecord.inversedMatching { return geositeRecord.policy.GetData() } } metadata := &C.Metadata{Host: domain} for _, domainSetRecord := range r.domainSetPolicy { if ok := domainSetRecord.domainSetProvider.Match(metadata); ok { return domainSetRecord.policy.GetData() } } 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() 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 atomic.TypedValue[string] ProxyAdapter C.ProxyAdapter ProxyName string Params map[string]string PreferH3 bool } 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 map[string][]NameServer DomainSetPolicy map[provider.RuleProvider][]NameServer GeositePolicy map[router.DomainMatcher][]NameServer } func NewResolver(config Config) *Resolver { defaultResolver := &Resolver{ main: transform(config.Default, nil), lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } r := &Resolver{ ipv6: config.IPv6, main: transform(config.Main, defaultResolver), lruCache: cache.New(cache.WithSize[string, *D.Msg](4096), cache.WithStale[string, *D.Msg](true)), hosts: config.Hosts, ipv6Timeout: time.Duration(config.IPv6Timeout) * time.Millisecond, } if len(config.Fallback) != 0 { r.fallback = transform(config.Fallback, defaultResolver) } if len(config.ProxyServer) != 0 { r.proxyServer = transform(config.ProxyServer, defaultResolver) } if len(config.Policy) != 0 { r.policy = trie.New[*Policy]() for domain, nameserver := range config.Policy { if strings.HasPrefix(strings.ToLower(domain), "geosite:") { groupname := domain[8:] inverse := false if strings.HasPrefix(groupname, "!") { inverse = true groupname = groupname[1:] } log.Debugln("adding geosite policy: %s inversed %t", groupname, inverse) matcher, err := NewGeoSite(groupname) if err != nil { continue } r.geositePolicy = append(r.geositePolicy, geositePolicyRecord{ matcher: matcher, policy: NewPolicy(transform(nameserver, defaultResolver)), inversedMatching: inverse, }) } else { _ = r.policy.Insert(domain, NewPolicy(transform(nameserver, defaultResolver))) } } r.policy.Optimize() } if len(config.DomainSetPolicy) > 0 { for p, n := range config.DomainSetPolicy { r.domainSetPolicy = append(r.domainSetPolicy, domainSetPolicyRecord{ domainSetProvider: p, policy: NewPolicy(transform(n, defaultResolver)), }) } } 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, lruCache: old.lruCache, hosts: old.hosts, policy: trie.New[*Policy](), ipv6Timeout: old.ipv6Timeout, } return r } var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go