mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-14 04:38:28 +10:00
811 lines
26 KiB
Go
811 lines
26 KiB
Go
package dns
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/netip"
|
|
"strings"
|
|
"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)
|
|
|
|
type Router struct {
|
|
ctx context.Context
|
|
logger logger.ContextLogger
|
|
transport adapter.DNSTransportManager
|
|
outbound adapter.OutboundManager
|
|
client adapter.DNSClient
|
|
rules []adapter.DNSRule
|
|
defaultDomainStrategy C.DomainStrategy
|
|
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
|
platformInterface adapter.PlatformInterface
|
|
legacyAddressFilterMode bool
|
|
}
|
|
|
|
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
|
router := &Router{
|
|
ctx: ctx,
|
|
logger: logFactory.NewLogger("dns"),
|
|
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
|
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
|
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
|
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
|
}
|
|
router.client = NewClient(ClientOptions{
|
|
DisableCache: options.DNSClientOptions.DisableCache,
|
|
DisableExpire: options.DNSClientOptions.DisableExpire,
|
|
IndependentCache: options.DNSClientOptions.IndependentCache,
|
|
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
|
|
},
|
|
Logger: router.logger,
|
|
})
|
|
if options.ReverseMapping {
|
|
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
|
|
}
|
|
return router
|
|
}
|
|
|
|
func (r *Router) Initialize(rules []option.DNSRule) error {
|
|
r.legacyAddressFilterMode = !hasNonLegacyAddressFilterItems(rules)
|
|
if !r.legacyAddressFilterMode {
|
|
err := validateNonLegacyAddressFilterRules(rules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for i, ruleOptions := range rules {
|
|
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, r.legacyAddressFilterMode)
|
|
if err != nil {
|
|
return E.Cause(err, "parse dns rule[", i, "]")
|
|
}
|
|
r.rules = append(r.rules, dnsRule)
|
|
}
|
|
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()
|
|
|
|
for i, rule := range r.rules {
|
|
monitor.Start("initialize DNS rule[", i, "]")
|
|
err := rule.Start()
|
|
monitor.Finish()
|
|
if err != nil {
|
|
return E.Cause(err, "initialize DNS rule[", i, "]")
|
|
}
|
|
}
|
|
if r.legacyAddressFilterMode && common.Any(r.rules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) {
|
|
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) Close() error {
|
|
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
|
var err error
|
|
for i, rule := range r.rules {
|
|
monitor.Start("close dns rule[", i, "]")
|
|
err = E.Append(err, rule.Close(), func(err error) error {
|
|
return E.Cause(err, "close dns rule[", i, "]")
|
|
})
|
|
monitor.Finish()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *Router) matchDNS(ctx context.Context, 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(r.rules); currentRuleIndex++ {
|
|
currentRule := r.rules[currentRuleIndex]
|
|
if currentRule.WithAddressLimit() && !isAddressQuery {
|
|
continue
|
|
}
|
|
metadata.ResetRuleCache()
|
|
metadata.DestinationAddressMatchFromResponse = false
|
|
if currentRule.Match(metadata) {
|
|
displayRuleIndex := currentRuleIndex
|
|
if displayRuleIndex != -1 {
|
|
displayRuleIndex += displayRuleIndex + 1
|
|
}
|
|
ruleDescription := currentRule.String()
|
|
if ruleDescription != "" {
|
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
|
} else {
|
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", 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
|
|
}
|
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
if options.Strategy == C.DomainStrategyAsIS {
|
|
options.Strategy = legacyTransport.LegacyStrategy()
|
|
}
|
|
if !options.ClientSubnet.IsValid() {
|
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
}
|
|
}
|
|
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()
|
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
if options.Strategy == C.DomainStrategyAsIS {
|
|
options.Strategy = legacyTransport.LegacyStrategy()
|
|
}
|
|
if !options.ClientSubnet.IsValid() {
|
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
}
|
|
}
|
|
return transport, nil, -1
|
|
}
|
|
|
|
func (r *Router) applyTransportDefaults(transport adapter.DNSTransport, options *adapter.DNSQueryOptions) {
|
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
if options.Strategy == C.DomainStrategyAsIS {
|
|
options.Strategy = legacyTransport.LegacyStrategy()
|
|
}
|
|
if !options.ClientSubnet.IsValid() {
|
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
|
|
if routeOptions.Strategy != C.DomainStrategyAsIS {
|
|
options.Strategy = routeOptions.Strategy
|
|
}
|
|
if routeOptions.DisableCache {
|
|
options.DisableCache = true
|
|
}
|
|
if routeOptions.RewriteTTL != nil {
|
|
options.RewriteTTL = routeOptions.RewriteTTL
|
|
}
|
|
if routeOptions.ClientSubnet.IsValid() {
|
|
options.ClientSubnet = routeOptions.ClientSubnet
|
|
}
|
|
}
|
|
|
|
func (r *Router) resolveDNSRoute(action *R.RuleActionDNSRoute, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, bool) {
|
|
transport, loaded := r.transport.Transport(action.Server)
|
|
if !loaded {
|
|
return nil, false
|
|
}
|
|
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
|
if isFakeIP && !allowFakeIP {
|
|
return transport, false
|
|
}
|
|
r.applyDNSRouteOptions(options, action.RuleActionDNSRouteOptions)
|
|
if isFakeIP {
|
|
options.DisableCache = true
|
|
}
|
|
r.applyTransportDefaults(transport, options)
|
|
return transport, true
|
|
}
|
|
|
|
func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) {
|
|
displayRuleIndex := ruleIndex
|
|
if displayRuleIndex != -1 {
|
|
displayRuleIndex += displayRuleIndex + 1
|
|
}
|
|
if ruleDescription := currentRule.String(); ruleDescription != "" {
|
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
|
} else {
|
|
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
|
}
|
|
}
|
|
|
|
func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) (*mDNS.Msg, adapter.DNSTransport, adapter.DNSQueryOptions, error) {
|
|
metadata := adapter.ContextFrom(ctx)
|
|
if metadata == nil {
|
|
panic("no context")
|
|
}
|
|
effectiveOptions := options
|
|
var savedResponse *mDNS.Msg
|
|
for currentRuleIndex, currentRule := range r.rules {
|
|
metadata.ResetRuleCache()
|
|
metadata.DNSResponse = savedResponse
|
|
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.resolveDNSRoute(&R.RuleActionDNSRoute{
|
|
Server: action.Server,
|
|
RuleActionDNSRouteOptions: action.RuleActionDNSRouteOptions,
|
|
}, allowFakeIP, &queryOptions)
|
|
if !loaded {
|
|
if transport == nil {
|
|
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
|
}
|
|
savedResponse = nil
|
|
continue
|
|
}
|
|
if queryOptions.Strategy == C.DomainStrategyAsIS {
|
|
queryOptions.Strategy = r.defaultDomainStrategy
|
|
}
|
|
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
|
|
if err != nil {
|
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
|
savedResponse = nil
|
|
continue
|
|
}
|
|
savedResponse = response
|
|
case *R.RuleActionDNSRoute:
|
|
queryOptions := effectiveOptions
|
|
transport, loaded := r.resolveDNSRoute(action, allowFakeIP, &queryOptions)
|
|
if !loaded {
|
|
if transport == nil {
|
|
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
|
}
|
|
continue
|
|
}
|
|
if queryOptions.Strategy == C.DomainStrategyAsIS {
|
|
queryOptions.Strategy = r.defaultDomainStrategy
|
|
}
|
|
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
|
|
return response, transport, queryOptions, err
|
|
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, effectiveOptions, nil
|
|
case C.RuleActionRejectMethodDrop:
|
|
return nil, nil, effectiveOptions, tun.ErrDrop
|
|
}
|
|
case *R.RuleActionPredefined:
|
|
return action.Response(message), nil, effectiveOptions, nil
|
|
}
|
|
}
|
|
queryOptions := effectiveOptions
|
|
transport := r.transport.Default()
|
|
r.applyTransportDefaults(transport, &queryOptions)
|
|
if queryOptions.Strategy == C.DomainStrategyAsIS {
|
|
queryOptions.Strategy = r.defaultDomainStrategy
|
|
}
|
|
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, queryOptions, nil)
|
|
return response, transport, queryOptions, err
|
|
}
|
|
|
|
type lookupWithRulesResponse struct {
|
|
addresses []netip.Addr
|
|
strategy C.DomainStrategy
|
|
}
|
|
|
|
func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions, strategies ...C.DomainStrategy) C.DomainStrategy {
|
|
if options.LookupStrategy != C.DomainStrategyAsIS {
|
|
return options.LookupStrategy
|
|
}
|
|
for _, strategy := range strategies {
|
|
if strategy != C.DomainStrategyAsIS {
|
|
return strategy
|
|
}
|
|
}
|
|
if options.Strategy != C.DomainStrategyAsIS {
|
|
return options.Strategy
|
|
}
|
|
return r.defaultDomainStrategy
|
|
}
|
|
|
|
func lookupStrategyAllowsQueryType(strategy C.DomainStrategy, qType uint16) bool {
|
|
switch strategy {
|
|
case C.DomainStrategyIPv4Only:
|
|
return qType == mDNS.TypeA
|
|
case C.DomainStrategyIPv6Only:
|
|
return qType == mDNS.TypeAAAA
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func (r *Router) lookupWithRules(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
|
lookupOptions := options
|
|
if options.LookupStrategy != C.DomainStrategyAsIS {
|
|
lookupOptions.Strategy = options.LookupStrategy
|
|
}
|
|
if options.LookupStrategy == C.DomainStrategyIPv4Only {
|
|
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
|
|
return response.addresses, err
|
|
}
|
|
if options.LookupStrategy == C.DomainStrategyIPv6Only {
|
|
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
|
|
return response.addresses, err
|
|
}
|
|
var (
|
|
response4 lookupWithRulesResponse
|
|
response6 lookupWithRulesResponse
|
|
)
|
|
var group task.Group
|
|
group.Append("exchange4", func(ctx context.Context) error {
|
|
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
|
|
response4 = result
|
|
return err
|
|
})
|
|
group.Append("exchange6", func(ctx context.Context) error {
|
|
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
|
|
response6 = result
|
|
return err
|
|
})
|
|
err := group.Run(ctx)
|
|
strategy := r.resolveLookupStrategy(options, response4.strategy, response6.strategy)
|
|
if !lookupStrategyAllowsQueryType(strategy, mDNS.TypeA) {
|
|
response4.addresses = nil
|
|
}
|
|
if !lookupStrategyAllowsQueryType(strategy, mDNS.TypeAAAA) {
|
|
response6.addresses = nil
|
|
}
|
|
if len(response4.addresses) == 0 && len(response6.addresses) == 0 {
|
|
return nil, err
|
|
}
|
|
return sortAddresses(response4.addresses, response6.addresses, strategy), nil
|
|
}
|
|
|
|
func (r *Router) lookupWithRulesType(ctx context.Context, domain string, qType uint16, options adapter.DNSQueryOptions) (lookupWithRulesResponse, error) {
|
|
request := &mDNS.Msg{
|
|
MsgHdr: mDNS.MsgHdr{
|
|
RecursionDesired: true,
|
|
},
|
|
Question: []mDNS.Question{{
|
|
Name: mDNS.Fqdn(FqdnToDomain(domain)),
|
|
Qtype: qType,
|
|
Qclass: mDNS.ClassINET,
|
|
}},
|
|
}
|
|
response, _, queryOptions, err := r.exchangeWithRules(adapter.OverrideContext(ctx), request, options, false)
|
|
result := lookupWithRulesResponse{
|
|
strategy: r.resolveLookupStrategy(options, queryOptions.Strategy),
|
|
}
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
if response.Rcode != mDNS.RcodeSuccess {
|
|
return result, RcodeError(response.Rcode)
|
|
}
|
|
if !lookupStrategyAllowsQueryType(result.strategy, qType) {
|
|
return result, nil
|
|
}
|
|
result.addresses = MessageToAddresses(response)
|
|
return result, 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.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
|
|
r.applyTransportDefaults(transport, &options)
|
|
if options.Strategy == C.DomainStrategyAsIS {
|
|
options.Strategy = r.defaultDomainStrategy
|
|
}
|
|
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
|
} else if !r.legacyAddressFilterMode {
|
|
response, transport, _, err = r.exchangeWithRules(ctx, message, options, true)
|
|
} else {
|
|
var (
|
|
rule adapter.DNSRule
|
|
ruleIndex int
|
|
)
|
|
ruleIndex = -1
|
|
for {
|
|
dnsCtx := adapter.OverrideContext(ctx)
|
|
dnsOptions := options
|
|
transport, rule, ruleIndex = r.matchDNS(ctx, 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:
|
|
return action.Response(message), nil
|
|
}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
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) {
|
|
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 {
|
|
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
|
|
r.applyTransportDefaults(transport, &options)
|
|
if options.Strategy == C.DomainStrategyAsIS {
|
|
options.Strategy = r.defaultDomainStrategy
|
|
}
|
|
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
|
} else if !r.legacyAddressFilterMode {
|
|
responseAddrs, err = r.lookupWithRules(ctx, 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, 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
|
|
checkMetadata.DNSResponse = response
|
|
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 hasNonLegacyAddressFilterItems(rules []option.DNSRule) bool {
|
|
return common.Any(rules, hasNonLegacyAddressFilterItemsInRule)
|
|
}
|
|
|
|
func hasNonLegacyAddressFilterItemsInRule(rule option.DNSRule) bool {
|
|
switch rule.Type {
|
|
case "", C.RuleTypeDefault:
|
|
return hasNonLegacyAddressFilterItemsInDefaultRule(rule.DefaultOptions)
|
|
case C.RuleTypeLogical:
|
|
action := rule.LogicalOptions.Action
|
|
return action == C.RuleActionTypeEvaluate || common.Any(rule.LogicalOptions.Rules, hasNonLegacyAddressFilterItemsInRule)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func hasNonLegacyAddressFilterItemsInDefaultRule(rule option.DefaultDNSRule) bool {
|
|
action := rule.Action
|
|
return action == C.RuleActionTypeEvaluate ||
|
|
rule.MatchResponse ||
|
|
rule.ResponseRcode != nil ||
|
|
len(rule.ResponseAnswer) > 0 ||
|
|
len(rule.ResponseNs) > 0 ||
|
|
len(rule.ResponseExtra) > 0
|
|
}
|
|
|
|
func validateNonLegacyAddressFilterRules(rules []option.DNSRule) error {
|
|
var seenEvaluate bool
|
|
for i, rule := range rules {
|
|
consumesResponse, err := validateNonLegacyAddressFilterRuleTree(rule)
|
|
if err != nil {
|
|
return E.Cause(err, "validate dns rule[", i, "]")
|
|
}
|
|
action := dnsRuleActionType(rule)
|
|
if action == C.RuleActionTypeEvaluate && consumesResponse {
|
|
return E.New("dns rule[", i, "]: evaluate rule cannot consume response state")
|
|
}
|
|
if consumesResponse && !seenEvaluate {
|
|
return E.New("dns rule[", i, "]: response matching requires a preceding top-level evaluate rule")
|
|
}
|
|
if action == C.RuleActionTypeEvaluate {
|
|
seenEvaluate = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNonLegacyAddressFilterRuleTree(rule option.DNSRule) (bool, error) {
|
|
switch rule.Type {
|
|
case "", C.RuleTypeDefault:
|
|
return validateNonLegacyAddressFilterDefaultRule(rule.DefaultOptions)
|
|
case C.RuleTypeLogical:
|
|
var consumesResponse bool
|
|
for i, subRule := range rule.LogicalOptions.Rules {
|
|
subConsumesResponse, err := validateNonLegacyAddressFilterRuleTree(subRule)
|
|
if err != nil {
|
|
return false, E.Cause(err, "sub rule[", i, "]")
|
|
}
|
|
consumesResponse = consumesResponse || subConsumesResponse
|
|
}
|
|
return consumesResponse, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func validateNonLegacyAddressFilterDefaultRule(rule option.DefaultDNSRule) (bool, error) {
|
|
hasResponseRecords := rule.ResponseRcode != nil ||
|
|
len(rule.ResponseAnswer) > 0 ||
|
|
len(rule.ResponseNs) > 0 ||
|
|
len(rule.ResponseExtra) > 0
|
|
if hasResponseRecords && !rule.MatchResponse {
|
|
return false, E.New("response_* items require match_response")
|
|
}
|
|
if (len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse {
|
|
return false, E.New("ip_cidr and ip_is_private require match_response in DNS evaluate mode")
|
|
}
|
|
if rule.IPAcceptAny {
|
|
return false, E.New("ip_accept_any is removed in DNS evaluate mode, use ip_cidr with match_response")
|
|
}
|
|
if rule.RuleSetIPCIDRAcceptEmpty {
|
|
return false, E.New("rule_set_ip_cidr_accept_empty is removed in DNS evaluate mode")
|
|
}
|
|
return rule.MatchResponse, nil
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
}
|