package dns import ( "context" "errors" "net/netip" "strings" "sync" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/contrab/freelru" "github.com/sagernet/sing/contrab/maphash" "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) var ( _ adapter.DNSRouter = (*Router)(nil) _ adapter.DNSRuleSetUpdateValidator = (*Router)(nil) ) type Router struct { ctx context.Context logger logger.ContextLogger transport adapter.DNSTransportManager outbound adapter.OutboundManager client adapter.DNSClient rawRules []option.DNSRule rules []adapter.DNSRule defaultDomainStrategy C.DomainStrategy dnsReverseMapping freelru.Cache[netip.Addr, string] platformInterface adapter.PlatformInterface legacyDNSMode bool rulesAccess sync.RWMutex started bool closing bool } func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("dns"), transport: service.FromContext[adapter.DNSTransportManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), rawRules: make([]option.DNSRule, 0, len(options.Rules)), rules: make([]adapter.DNSRule, 0, len(options.Rules)), defaultDomainStrategy: C.DomainStrategy(options.Strategy), } if options.DNSClientOptions.IndependentCache { deprecated.Report(ctx, deprecated.OptionIndependentDNSCache) } var optimisticTimeout time.Duration optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic) if optimisticOptions.Enabled { if options.DNSClientOptions.DisableCache { return nil, E.New("`optimistic` is conflict with `disable_cache`") } if options.DNSClientOptions.DisableExpire { return nil, E.New("`optimistic` is conflict with `disable_expire`") } optimisticTimeout = time.Duration(optimisticOptions.Timeout) if optimisticTimeout == 0 { optimisticTimeout = 3 * 24 * time.Hour } } router.client = NewClient(ClientOptions{ Context: ctx, DisableCache: options.DNSClientOptions.DisableCache, DisableExpire: options.DNSClientOptions.DisableExpire, OptimisticTimeout: optimisticTimeout, CacheCapacity: options.DNSClientOptions.CacheCapacity, ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}), RDRC: func() adapter.RDRCStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { return nil } if !cacheFile.StoreRDRC() { return nil } return cacheFile }, DNSCache: func() adapter.DNSCacheStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { return nil } if !cacheFile.StoreDNS() { return nil } cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire) cacheFile.SetOptimisticTimeout(optimisticTimeout) return cacheFile }, Logger: router.logger, }) if options.ReverseMapping { router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32)) } return router, nil } func (r *Router) Initialize(rules []option.DNSRule) error { r.rawRules = append(r.rawRules[:0], rules...) newRules, _, _, err := r.buildRules(false) if err != nil { return err } closeRules(newRules) return nil } func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateStart: monitor.Start("initialize DNS client") r.client.Start() monitor.Finish() monitor.Start("initialize DNS rules") newRules, legacyDNSMode, modeFlags, err := r.buildRules(true) monitor.Finish() if err != nil { return err } r.rulesAccess.Lock() if r.closing { r.rulesAccess.Unlock() closeRules(newRules) return nil } r.rules = newRules r.legacyDNSMode = legacyDNSMode r.started = true r.rulesAccess.Unlock() if legacyDNSMode && common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) { deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter) } if legacyDNSMode && modeFlags.neededFromStrategy { deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy) } } return nil } func (r *Router) Close() error { r.rulesAccess.Lock() if r.closing { r.rulesAccess.Unlock() return nil } r.closing = true runtimeRules := r.rules r.rules = nil r.rulesAccess.Unlock() closeRules(runtimeRules) return nil } func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) { for i, ruleOptions := range r.rawRules { err := R.ValidateNoNestedDNSRuleActions(ruleOptions) if err != nil { return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") } } router := service.FromContext[adapter.Router](r.ctx) legacyDNSMode, modeFlags, err := resolveLegacyDNSMode(router, r.rawRules, nil) if err != nil { return nil, false, dnsRuleModeFlags{}, err } if !legacyDNSMode { err = validateLegacyDNSModeDisabledRules(r.rawRules) if err != nil { return nil, false, dnsRuleModeFlags{}, err } } err = validateEvaluateFakeIPRules(r.rawRules, r.transport) if err != nil { return nil, false, dnsRuleModeFlags{}, err } newRules := make([]adapter.DNSRule, 0, len(r.rawRules)) for i, ruleOptions := range r.rawRules { var dnsRule adapter.DNSRule dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode) if err != nil { closeRules(newRules) return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]") } newRules = append(newRules, dnsRule) } if startRules { for i, rule := range newRules { err = rule.Start() if err != nil { closeRules(newRules) return nil, false, dnsRuleModeFlags{}, E.Cause(err, "initialize DNS rule[", i, "]") } } } return newRules, legacyDNSMode, modeFlags, nil } func closeRules(rules []adapter.DNSRule) { for _, rule := range rules { _ = rule.Close() } } func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error { if len(r.rawRules) == 0 { return nil } router := service.FromContext[adapter.Router](r.ctx) if router == nil { return E.New("router service not found") } overrides := map[string]adapter.RuleSetMetadata{ tag: metadata, } r.rulesAccess.RLock() started := r.started legacyDNSMode := r.legacyDNSMode closing := r.closing r.rulesAccess.RUnlock() if closing { return nil } if !started { candidateLegacyDNSMode, _, err := resolveLegacyDNSMode(router, r.rawRules, overrides) if err != nil { return err } if !candidateLegacyDNSMode { return validateLegacyDNSModeDisabledRules(r.rawRules) } return nil } candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides) if err != nil { return err } if legacyDNSMode { if !candidateLegacyDNSMode && flags.disabled { err := validateLegacyDNSModeDisabledRules(r.rawRules) if err != nil { return err } return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } return nil } if candidateLegacyDNSMode { return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink()) } return nil } func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } var currentRuleIndex int if ruleIndex != -1 { currentRuleIndex = ruleIndex + 1 } for ; currentRuleIndex < len(rules); currentRuleIndex++ { currentRule := rules[currentRuleIndex] if currentRule.WithAddressLimit() && !isAddressQuery { continue } metadata.ResetRuleCache() metadata.DestinationAddressMatchFromResponse = false if currentRule.LegacyPreMatch(metadata) { if ruleDescription := currentRule.String(); ruleDescription != "" { r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) } switch action := currentRule.Action().(type) { case *R.RuleActionDNSRoute: transport, loaded := r.transport.Transport(action.Server) if !loaded { r.logger.ErrorContext(ctx, "transport not found: ", action.Server) continue } isFakeIP := transport.Type() == C.DNSTypeFakeIP if isFakeIP && !allowFakeIP { continue } if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } if isFakeIP || action.DisableCache { options.DisableCache = true } if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } return transport, currentRule, currentRuleIndex case *R.RuleActionDNSRouteOptions: if action.Strategy != C.DomainStrategyAsIS { options.Strategy = action.Strategy } if action.DisableCache { options.DisableCache = true } if action.RewriteTTL != nil { options.RewriteTTL = action.RewriteTTL } if action.ClientSubnet.IsValid() { options.ClientSubnet = action.ClientSubnet } case *R.RuleActionReject: return nil, currentRule, currentRuleIndex case *R.RuleActionPredefined: return nil, currentRule, currentRuleIndex } } } transport := r.transport.Default() return transport, nil, -1 } func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) { // Strategy is intentionally skipped here. A non-default DNS rule action strategy // forces legacy mode via resolveLegacyDNSMode, so this path is only reachable // when strategy remains at its default value. if routeOptions.DisableCache { options.DisableCache = true } if routeOptions.DisableOptimisticCache { options.DisableOptimisticCache = true } if routeOptions.RewriteTTL != nil { options.RewriteTTL = routeOptions.RewriteTTL } if routeOptions.ClientSubnet.IsValid() { options.ClientSubnet = routeOptions.ClientSubnet } } type dnsRouteStatus uint8 const ( dnsRouteStatusMissing dnsRouteStatus = iota dnsRouteStatusSkipped dnsRouteStatusResolved ) func (r *Router) resolveDNSRoute(server string, routeOptions R.RuleActionDNSRouteOptions, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) { transport, loaded := r.transport.Transport(server) if !loaded { return nil, dnsRouteStatusMissing } isFakeIP := transport.Type() == C.DNSTypeFakeIP if isFakeIP && !allowFakeIP { return transport, dnsRouteStatusSkipped } r.applyDNSRouteOptions(options, routeOptions) if isFakeIP { options.DisableCache = true } return transport, dnsRouteStatusResolved } func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) { if ruleDescription := currentRule.String(); ruleDescription != "" { r.logger.DebugContext(ctx, "match[", ruleIndex, "] ", currentRule, " => ", currentRule.Action()) } else { r.logger.DebugContext(ctx, "match[", ruleIndex, "] => ", currentRule.Action()) } } type exchangeWithRulesResult struct { response *mDNS.Msg transport adapter.DNSTransport rejectAction *R.RuleActionReject err error } const dnsRespondMissingResponseMessage = "respond action requires an evaluated response from a preceding evaluate action" func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } effectiveOptions := options var evaluatedResponse *mDNS.Msg var evaluatedTransport adapter.DNSTransport for currentRuleIndex, currentRule := range rules { metadata.ResetRuleCache() metadata.DNSResponse = evaluatedResponse metadata.DestinationAddressMatchFromResponse = false if !currentRule.Match(metadata) { continue } r.logRuleMatch(ctx, currentRuleIndex, currentRule) switch action := currentRule.Action().(type) { case *R.RuleActionDNSRouteOptions: r.applyDNSRouteOptions(&effectiveOptions, *action) case *R.RuleActionEvaluate: queryOptions := effectiveOptions transport, loaded := r.transport.Transport(action.Server) if !loaded { r.logger.ErrorContext(ctx, "transport not found: ", action.Server) evaluatedResponse = nil evaluatedTransport = nil continue } r.applyDNSRouteOptions(&queryOptions, action.RuleActionDNSRouteOptions) exchangeOptions := queryOptions if exchangeOptions.Strategy == C.DomainStrategyAsIS { exchangeOptions.Strategy = r.defaultDomainStrategy } response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) if err != nil { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) evaluatedResponse = nil evaluatedTransport = nil continue } evaluatedResponse = response evaluatedTransport = transport case *R.RuleActionRespond: if evaluatedResponse == nil { return exchangeWithRulesResult{ err: E.New(dnsRespondMissingResponseMessage), } } return exchangeWithRulesResult{ response: evaluatedResponse, transport: evaluatedTransport, } case *R.RuleActionDNSRoute: queryOptions := effectiveOptions transport, status := r.resolveDNSRoute(action.Server, action.RuleActionDNSRouteOptions, allowFakeIP, &queryOptions) switch status { case dnsRouteStatusMissing: r.logger.ErrorContext(ctx, "transport not found: ", action.Server) continue case dnsRouteStatusSkipped: continue } exchangeOptions := queryOptions if exchangeOptions.Strategy == C.DomainStrategyAsIS { exchangeOptions.Strategy = r.defaultDomainStrategy } response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) return exchangeWithRulesResult{ response: response, transport: transport, err: err, } case *R.RuleActionReject: switch action.Method { case C.RuleActionRejectMethodDefault: return exchangeWithRulesResult{ response: &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Rcode: mDNS.RcodeRefused, Response: true, }, Question: []mDNS.Question{message.Question[0]}, }, rejectAction: action, } case C.RuleActionRejectMethodDrop: return exchangeWithRulesResult{ rejectAction: action, err: tun.ErrDrop, } } case *R.RuleActionPredefined: return exchangeWithRulesResult{ response: action.Response(message), } } } transport := r.transport.Default() exchangeOptions := effectiveOptions if exchangeOptions.Strategy == C.DomainStrategyAsIS { exchangeOptions.Strategy = r.defaultDomainStrategy } response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil) return exchangeWithRulesResult{ response: response, transport: transport, err: err, } } func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.DomainStrategy { if options.LookupStrategy != C.DomainStrategyAsIS { return options.LookupStrategy } if options.Strategy != C.DomainStrategyAsIS { return options.Strategy } return r.defaultDomainStrategy } func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context { ctx, metadata := adapter.ExtendContext(ctx) metadata.QueryType = qType metadata.IPVersion = 0 switch qType { case mDNS.TypeA: metadata.IPVersion = 4 case mDNS.TypeAAAA: metadata.IPVersion = 6 } return ctx } func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Addr { switch qType { case mDNS.TypeA: return common.Filter(addresses, func(address netip.Addr) bool { return address.Is4() }) case mDNS.TypeAAAA: return common.Filter(addresses, func(address netip.Addr) bool { return address.Is6() }) default: return addresses } } func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { strategy := r.resolveLookupStrategy(options) lookupOptions := options if strategy != C.DomainStrategyAsIS { lookupOptions.Strategy = strategy } if strategy == C.DomainStrategyIPv4Only { return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) } if strategy == C.DomainStrategyIPv6Only { return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) } var ( response4 []netip.Addr response6 []netip.Addr ) var group task.Group group.Append("exchange4", func(ctx context.Context) error { result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions) response4 = result return err }) group.Append("exchange6", func(ctx context.Context) error { result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions) response6 = result return err }) err := group.Run(ctx) if len(response4) == 0 && len(response6) == 0 { return nil, err } return sortAddresses(response4, response6, strategy), nil } func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) ([]netip.Addr, error) { request := &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ RecursionDesired: true, }, Question: []mDNS.Question{{ Name: mDNS.Fqdn(domain), Qtype: qType, Qclass: mDNS.ClassINET, }}, } exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false) if exchangeResult.rejectAction != nil { return nil, exchangeResult.rejectAction.Error(ctx) } if exchangeResult.err != nil { return nil, exchangeResult.err } if exchangeResult.response.Rcode != mDNS.RcodeSuccess { return nil, RcodeError(exchangeResult.response.Rcode) } return filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType), nil } func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) { if len(message.Question) != 1 { r.logger.WarnContext(ctx, "bad question size: ", len(message.Question)) responseMessage := mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Response: true, Rcode: mDNS.RcodeFormatError, }, Question: message.Question, } return &responseMessage, nil } r.rulesAccess.RLock() if r.closing { r.rulesAccess.RUnlock() return nil, E.New("dns router closed") } rules := r.rules legacyDNSMode := r.legacyDNSMode r.rulesAccess.RUnlock() r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String())) var ( response *mDNS.Msg transport adapter.DNSTransport err error ) var metadata *adapter.InboundContext ctx, metadata = adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} metadata.QueryType = message.Question[0].Qtype metadata.DNSResponse = nil metadata.DestinationAddressMatchFromResponse = false switch metadata.QueryType { case mDNS.TypeA: metadata.IPVersion = 4 case mDNS.TypeAAAA: metadata.IPVersion = 6 } metadata.Domain = FqdnToDomain(message.Question[0].Name) if options.Transport != nil { transport = options.Transport if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } response, err = r.client.Exchange(ctx, transport, message, options, nil) } else if !legacyDNSMode { exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true) response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err } else { var ( rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: switch action.Method { case C.RuleActionRejectMethodDefault: return &mDNS.Msg{ MsgHdr: mDNS.MsgHdr{ Id: message.Id, Rcode: mDNS.RcodeRefused, Response: true, }, Question: []mDNS.Question{message.Question[0]}, }, nil case C.RuleActionRejectMethodDrop: return nil, tun.ErrDrop } case *R.RuleActionPredefined: err = nil response = action.Response(message) goto done } } responseCheck := addressLimitResponseCheck(rule, metadata) if dnsOptions.Strategy == C.DomainStrategyAsIS { dnsOptions.Strategy = r.defaultDomainStrategy } response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck) var rejected bool if err != nil { if errors.Is(err, ErrResponseRejectedCached) { rejected = true r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)") } else if errors.Is(err, ErrResponseRejected) { rejected = true r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) } else if len(message.Question) > 0 { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) } else { r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ")) } } if responseCheck != nil && rejected { continue } break } } done: if err != nil { return nil, err } if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { if transport == nil || transport.Type() != C.DNSTypeFakeIP { for _, answer := range response.Answer { switch record := answer.(type) { case *mDNS.A: r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.A), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) case *mDNS.AAAA: r.dnsReverseMapping.AddWithLifetime(M.AddrFromIP(record.AAAA), FqdnToDomain(record.Hdr.Name), time.Duration(record.Hdr.Ttl)*time.Second) } } } } return response, nil } func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { r.rulesAccess.RLock() if r.closing { r.rulesAccess.RUnlock() return nil, E.New("dns router closed") } rules := r.rules legacyDNSMode := r.legacyDNSMode r.rulesAccess.RUnlock() var ( responseAddrs []netip.Addr err error ) printResult := func() { if err == nil && len(responseAddrs) == 0 { err = E.New("empty result") } if err != nil { if errors.Is(err, ErrResponseRejectedCached) { r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)") } else if errors.Is(err, ErrResponseRejected) { r.logger.DebugContext(ctx, "response rejected for ", domain) } else if R.IsRejected(err) { r.logger.DebugContext(ctx, "lookup rejected for ", domain) } else { r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) } } if err != nil { err = E.Cause(err, "lookup ", domain) } } r.logger.DebugContext(ctx, "lookup domain ", domain) ctx, metadata := adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} metadata.Domain = FqdnToDomain(domain) metadata.DNSResponse = nil metadata.DestinationAddressMatchFromResponse = false if options.Transport != nil { transport := options.Transport if options.Strategy == C.DomainStrategyAsIS { options.Strategy = r.defaultDomainStrategy } responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil) } else if !legacyDNSMode { responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options) } else { var ( transport adapter.DNSTransport rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { dnsCtx := adapter.OverrideContext(ctx) dnsOptions := options transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions) if rule != nil { switch action := rule.Action().(type) { case *R.RuleActionReject: return nil, &R.RejectedError{Cause: action.Error(ctx)} case *R.RuleActionPredefined: responseAddrs = nil if action.Rcode != mDNS.RcodeSuccess { err = RcodeError(action.Rcode) } else { err = nil for _, answer := range action.Answer { switch record := answer.(type) { case *mDNS.A: responseAddrs = append(responseAddrs, M.AddrFromIP(record.A)) case *mDNS.AAAA: responseAddrs = append(responseAddrs, M.AddrFromIP(record.AAAA)) } } } goto response } } responseCheck := addressLimitResponseCheck(rule, metadata) if dnsOptions.Strategy == C.DomainStrategyAsIS { dnsOptions.Strategy = r.defaultDomainStrategy } responseAddrs, err = r.client.Lookup(dnsCtx, transport, domain, dnsOptions, responseCheck) if responseCheck == nil || err == nil { break } printResult() } } response: printResult() if len(responseAddrs) > 0 { r.logger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) } return responseAddrs, err } func isAddressQuery(message *mDNS.Msg) bool { for _, question := range message.Question { if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA || question.Qtype == mDNS.TypeHTTPS { return true } } return false } func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool { if rule == nil || !rule.WithAddressLimit() { return nil } responseMetadata := *metadata return func(response *mDNS.Msg) bool { checkMetadata := responseMetadata return rule.MatchAddressLimit(&checkMetadata, response) } } func (r *Router) ClearCache() { r.client.ClearCache() if r.platformInterface != nil { r.platformInterface.ClearDNSCache() } } func (r *Router) LookupReverseMapping(ip netip.Addr) (string, bool) { if r.dnsReverseMapping == nil { return "", false } domain, loaded := r.dnsReverseMapping.Get(ip) return domain, loaded } func (r *Router) ResetNetwork() { r.ClearCache() for _, transport := range r.transport.Transports() { transport.Reset() } } func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool { if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return true } return !rule.MatchResponse && (rule.IPAcceptAny || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) } func hasResponseMatchFields(rule option.DefaultDNSRule) bool { return rule.ResponseRcode != nil || len(rule.ResponseAnswer) > 0 || len(rule.ResponseNs) > 0 || len(rule.ResponseExtra) > 0 } func defaultRuleDisablesLegacyDNSMode(rule option.DefaultDNSRule) bool { return rule.MatchResponse || hasResponseMatchFields(rule) || rule.Action == C.RuleActionTypeEvaluate || rule.Action == C.RuleActionTypeRespond || rule.IPVersion > 0 || len(rule.QueryType) > 0 } type dnsRuleModeFlags struct { disabled bool needed bool neededFromStrategy bool } func (f *dnsRuleModeFlags) merge(other dnsRuleModeFlags) { f.disabled = f.disabled || other.disabled f.needed = f.needed || other.needed f.neededFromStrategy = f.neededFromStrategy || other.neededFromStrategy } func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, dnsRuleModeFlags, error) { flags, err := dnsRuleModeRequirements(router, rules, metadataOverrides) if err != nil { return false, flags, err } if flags.disabled && flags.neededFromStrategy { return false, flags, E.New(deprecated.OptionLegacyDNSRuleStrategy.MessageWithLink()) } if flags.disabled { return false, flags, nil } return flags.needed, flags, nil } func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { var flags dnsRuleModeFlags for i, rule := range rules { ruleFlags, err := dnsRuleModeRequirementsInRule(router, rule, metadataOverrides) if err != nil { return dnsRuleModeFlags{}, E.Cause(err, "dns rule[", i, "]") } flags.merge(ruleFlags) } return flags, nil } func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { switch rule.Type { case "", C.RuleTypeDefault: return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides) case C.RuleTypeLogical: flags := dnsRuleModeFlags{ disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond || dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction), } flags.needed = flags.neededFromStrategy for i, subRule := range rule.LogicalOptions.Rules { subFlags, err := dnsRuleModeRequirementsInRule(router, subRule, metadataOverrides) if err != nil { return dnsRuleModeFlags{}, E.Cause(err, "sub rule[", i, "]") } flags.merge(subFlags) } return flags, nil default: return dnsRuleModeFlags{}, nil } } func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) { flags := dnsRuleModeFlags{ disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction), neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction), } flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy if len(rule.RuleSet) == 0 { return flags, nil } if router == nil { return dnsRuleModeFlags{}, E.New("router service not found") } for _, tag := range rule.RuleSet { metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides) if err != nil { return dnsRuleModeFlags{}, err } // ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent. flags.disabled = flags.disabled || metadata.ContainsDNSQueryTypeRule if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule { flags.needed = true } } return flags, nil } func lookupDNSRuleSetMetadata(router adapter.Router, tag string, metadataOverrides map[string]adapter.RuleSetMetadata) (adapter.RuleSetMetadata, error) { if metadataOverrides != nil { if metadata, loaded := metadataOverrides[tag]; loaded { return metadata, nil } } ruleSet, loaded := router.RuleSet(tag) if !loaded { return adapter.RuleSetMetadata{}, E.New("rule-set not found: ", tag) } return ruleSet.Metadata(), nil } func referencedDNSRuleSetTags(rules []option.DNSRule) []string { tagMap := make(map[string]bool) var walkRule func(rule option.DNSRule) walkRule = func(rule option.DNSRule) { switch rule.Type { case "", C.RuleTypeDefault: for _, tag := range rule.DefaultOptions.RuleSet { tagMap[tag] = true } case C.RuleTypeLogical: for _, subRule := range rule.LogicalOptions.Rules { walkRule(subRule) } } } for _, rule := range rules { walkRule(rule) } tags := make([]string, 0, len(tagMap)) for tag := range tagMap { if tag != "" { tags = append(tags, tag) } } return tags } func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error { var seenEvaluate bool for i, rule := range rules { requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule) if err != nil { return E.Cause(err, "validate dns rule[", i, "]") } if requiresPriorEvaluate && !seenEvaluate { return E.New("dns rule[", i, "]: response-based matching requires a preceding evaluate action") } if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate { seenEvaluate = true } } return nil } func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapter.DNSTransportManager) error { if transportManager == nil { return nil } for i, rule := range rules { if dnsRuleActionType(rule) != C.RuleActionTypeEvaluate { continue } server := dnsRuleActionServer(rule) if server == "" { continue } transport, loaded := transportManager.Transport(server) if !loaded || transport.Type() != C.DNSTypeFakeIP { continue } return E.New("dns rule[", i, "]: evaluate action cannot use fakeip server: ", server) } return nil } func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) { switch rule.Type { case "", C.RuleTypeDefault: return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions) case C.RuleTypeLogical: requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond for i, subRule := range rule.LogicalOptions.Rules { subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule) if err != nil { return false, E.Cause(err, "sub rule[", i, "]") } requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate } return requiresPriorEvaluate, nil default: return false, nil } } func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) { hasResponseRecords := hasResponseMatchFields(rule) if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse { return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled") } // Intentionally do not reject rule_set here. A referenced rule set may mix // destination-IP predicates with pre-response predicates such as domain items. // When match_response is false, those destination-IP branches fail closed during // pre-response evaluation instead of consuming DNS response state, while sibling // non-response branches remain matchable. if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink()) } return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil } func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool { switch action.Action { case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: return action.RouteOptions.DisableOptimisticCache case C.RuleActionTypeRouteOptions: return action.RouteOptionsOptions.DisableOptimisticCache default: return false } } func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool { switch action.Action { case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate: return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS case C.RuleActionTypeRouteOptions: return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS default: return false } } func dnsRuleActionType(rule option.DNSRule) string { switch rule.Type { case "", C.RuleTypeDefault: if rule.DefaultOptions.Action == "" { return C.RuleActionTypeRoute } return rule.DefaultOptions.Action case C.RuleTypeLogical: if rule.LogicalOptions.Action == "" { return C.RuleActionTypeRoute } return rule.LogicalOptions.Action default: return "" } } func dnsRuleActionServer(rule option.DNSRule) string { switch rule.Type { case "", C.RuleTypeDefault: return rule.DefaultOptions.RouteOptions.Server case C.RuleTypeLogical: return rule.LogicalOptions.RouteOptions.Server default: return "" } }