| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158 |
- 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(router, r.rawRules, nil)
- 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(router, r.rawRules, overrides)
- }
- return nil
- }
- candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides)
- if err != nil {
- return err
- }
- if legacyDNSMode {
- if !candidateLegacyDNSMode && flags.disabled {
- err := validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides)
- if err != nil {
- return err
- }
- return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
- }
- return nil
- }
- if candidateLegacyDNSMode {
- return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
- }
- return validateLegacyDNSModeDisabledRules(router, r.rawRules, overrides)
- }
- 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 <empty query>"))
- }
- }
- 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(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) error {
- var seenEvaluate bool
- for i, rule := range rules {
- requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, rule, metadataOverrides)
- 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(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, error) {
- switch rule.Type {
- case "", C.RuleTypeDefault:
- return validateLegacyDNSModeDisabledDefaultRule(router, rule.DefaultOptions, metadataOverrides)
- case C.RuleTypeLogical:
- requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond
- for i, subRule := range rule.LogicalOptions.Rules {
- subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(router, subRule, metadataOverrides)
- if err != nil {
- return false, E.Cause(err, "sub rule[", i, "]")
- }
- requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate
- }
- return requiresPriorEvaluate, nil
- default:
- return false, nil
- }
- }
- func validateLegacyDNSModeDisabledDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (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")
- }
- // rule_set entries are only rejected when every referenced set is pure-IP;
- // mixed sets still fall through because their non-IP branches remain matchable
- // before a DNS response is available.
- if !rule.MatchResponse && len(rule.RuleSet) > 0 {
- for _, tag := range rule.RuleSet {
- metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides)
- if err != nil {
- return false, err
- }
- if metadata.ContainsIPCIDRRule && !metadata.ContainsNonIPCIDRRule {
- return false, E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
- }
- }
- }
- 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 ""
- }
- }
|