Files
sing-box-extended-mirror/route/rule/rule_dns.go
T
世界 08986ad405 Fix address filter matching for inverted legacy DNS rules
Inverted rules whose address filter conditions come from rule sets or
logical rules were rejected during pre-lookup matching, so the lookup was
never sent and the rules never matched; for plain ip rule sets this was a
regression from the rule-set invert fix in 1.12.22. Pre-lookup matching
now tracks whether the result depends on deferred address filter
conditions and defers such rules to the response phase, while results
already determined by evaluated conditions are still decided during
pre-lookup.
2026-08-30 17:41:46 +08:00

563 lines
19 KiB
Go

package rule
import (
"context"
"github.com/sagernet/sing-box/adapter"
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"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool, legacyDNSMode bool) (adapter.DNSRule, error) {
switch options.Type {
case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions")
}
if !checkServer && options.DefaultOptions.Action == C.RuleActionTypeEvaluate {
return nil, E.New(options.DefaultOptions.Action, " is only allowed on top-level DNS rules")
}
err := validateDNSRuleAction(options.DefaultOptions.DNSRuleAction)
if err != nil {
return nil, err
}
if options.DefaultOptions.Race && !options.DefaultOptions.MatchResponse.IsEnabled() {
return nil, E.New("`race` requires `match_response`")
}
switch options.DefaultOptions.Action {
case "", C.RuleActionTypeRoute:
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
case C.RuleActionTypeEvaluate:
if options.DefaultOptions.EvaluateOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode)
case C.RuleTypeLogical:
if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions")
}
if !checkServer && options.LogicalOptions.Action == C.RuleActionTypeEvaluate {
return nil, E.New(options.LogicalOptions.Action, " is only allowed on top-level DNS rules")
}
err := validateDNSRuleAction(options.LogicalOptions.DNSRuleAction)
if err != nil {
return nil, err
}
switch options.LogicalOptions.Action {
case "", C.RuleActionTypeRoute:
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
case C.RuleActionTypeEvaluate:
if options.LogicalOptions.EvaluateOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode)
default:
return nil, E.New("unknown rule type: ", options.Type)
}
}
func validateDNSRuleAction(action option.DNSRuleAction) error {
if action.Action == C.RuleActionTypeReject && action.RejectOptions.Method == C.RuleActionRejectMethodReply {
return E.New("reject method `reply` is not supported for DNS rules")
}
var routeOptions option.AbstractDNSRouteActionOptions
switch action.Action {
case "", C.RuleActionTypeRoute:
routeOptions = action.RouteOptions.AbstractDNSRouteActionOptions
case C.RuleActionTypeEvaluate:
routeOptions = action.EvaluateOptions.AbstractDNSRouteActionOptions
case C.RuleActionTypeRouteOptions:
routeOptions = option.AbstractDNSRouteActionOptions(action.RouteOptionsOptions)
}
if routeOptions.RemoveClientSubnet && routeOptions.ClientSubnet != nil {
return E.New("`client_subnet` and `remove_client_subnet` are mutually exclusive")
}
if action.Race {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeRespond, C.RuleActionTypeReject, C.RuleActionTypePredefined:
default:
return E.New("`race` requires a final action")
}
if action.RouteOptions.Speculative {
return E.New("`race` and `speculative` cannot be combined on the same rule")
}
}
return nil
}
var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
type DefaultDNSRule struct {
abstractDefaultRule
matchResponse bool
matchResponseTag string
race bool
}
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule, legacyDNSMode bool) (*DefaultDNSRule, error) {
rule := &DefaultDNSRule{
abstractDefaultRule: abstractDefaultRule{
invert: options.Invert,
action: NewDNSRuleAction(logger, options.DNSRuleAction),
},
matchResponse: options.MatchResponse.IsEnabled(),
matchResponseTag: options.MatchResponse.ResponseTag(),
race: options.Race,
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
router := service.FromContext[adapter.Router](ctx)
networkManager := service.FromContext[adapter.NetworkManager](ctx)
if options.IPVersion > 0 {
switch options.IPVersion {
case 4, 6:
item := NewIPVersionItem(options.IPVersion == 6)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
default:
return nil, E.New("invalid ip version: ", options.IPVersion)
}
}
if len(options.QueryType) > 0 {
item := NewQueryTypeItem(options.QueryType)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.QueryClientSubnet) > 0 {
item := NewQueryClientSubnetItem(options.QueryClientSubnet)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.QueryDNSSEC {
item := NewQueryDNSSECItem()
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.AuthUser) > 0 {
item := NewAuthUserItem(options.AuthUser)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Protocol) > 0 {
item := NewProtocolItem(options.Protocol)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
if err != nil {
return nil, err
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainKeyword) > 0 {
item := NewDomainKeywordItem(options.DomainKeyword)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainRegex) > 0 {
item, err := NewDomainRegexItem(options.DomainRegex)
if err != nil {
return nil, E.Cause(err, "domain_regex")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Geosite) > 0 { //nolint:staticcheck
return nil, E.New("geosite database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0")
}
if len(options.SourceGeoIP) > 0 {
return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0")
}
if len(options.GeoIP) > 0 {
return nil, E.New("geoip database is deprecated in sing-box 1.8.0 and removed in sing-box 1.12.0")
}
if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil {
return nil, E.Cause(err, "source_ip_cidr")
}
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil {
return nil, E.Cause(err, "ip_cidr")
}
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.SourceIPIsPrivate {
item := NewIPIsPrivateItem(true)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPIsPrivate {
item := NewIPIsPrivateItem(false)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPAcceptAny {
item := NewIPAcceptAnyItem()
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.ResponseRcode != nil {
item := NewDNSResponseRCodeItem(int(*options.ResponseRcode))
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseAnswer) > 0 {
item := NewDNSResponseRecordItem("response_answer", options.ResponseAnswer, dnsResponseAnswers)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseNs) > 0 {
item := NewDNSResponseRecordItem("response_ns", options.ResponseNs, dnsResponseNS)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseExtra) > 0 {
item := NewDNSResponseRecordItem("response_extra", options.ResponseExtra, dnsResponseExtra)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePortRange) > 0 {
item, err := NewPortRangeItem(true, options.SourcePortRange)
if err != nil {
return nil, E.Cause(err, "source_port_range")
}
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Port) > 0 {
item := NewPortItem(false, options.Port)
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PortRange) > 0 {
item, err := NewPortRangeItem(false, options.PortRange)
if err != nil {
return nil, E.Cause(err, "port_range")
}
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ProcessName) > 0 {
item := NewProcessItem(options.ProcessName)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ProcessPath) > 0 {
item := NewProcessPathItem(options.ProcessPath)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ProcessPathRegex) > 0 {
item, err := NewProcessPathRegexItem(options.ProcessPathRegex)
if err != nil {
return nil, E.Cause(err, "process_path_regex")
}
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PackageName) > 0 {
item := NewPackageNameItem(options.PackageName)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PackageNameRegex) > 0 {
item, err := NewPackageNameRegexItem(options.PackageNameRegex)
if err != nil {
return nil, E.Cause(err, "package_name_regex")
}
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.User) > 0 {
item := NewUserItem(options.User)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.UserID) > 0 {
item := NewUserIDItem(options.UserID)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Outbound) > 0 {
item := NewOutboundRule(ctx, options.Outbound)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.ClashMode != "" {
item := NewClashModeItem(ctx, options.ClashMode)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.NetworkType) > 0 {
item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build))
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.NetworkIsExpensive {
item := NewNetworkIsExpensiveItem(networkManager)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.NetworkIsConstrained {
item := NewNetworkIsConstrainedItem(networkManager)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.WIFISSID) > 0 {
item := NewWIFISSIDItem(networkManager, options.WIFISSID)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.WIFIBSSID) > 0 {
item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.InterfaceAddress != nil && options.InterfaceAddress.Size() > 0 {
item := NewInterfaceAddressItem(networkManager, options.InterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.NetworkInterfaceAddress != nil && options.NetworkInterfaceAddress.Size() > 0 {
item := NewNetworkInterfaceAddressItem(networkManager, options.NetworkInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DefaultInterfaceAddress) > 0 {
item := NewDefaultInterfaceAddressItem(networkManager, options.DefaultInterfaceAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceMACAddress) > 0 {
item := NewSourceMACAddressItem(options.SourceMACAddress)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceHostname) > 0 {
item := NewSourceHostnameItem(options.SourceHostname)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PreferredBy) > 0 {
item := NewPreferredByDNSItem(ctx, options.PreferredBy)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty)
} else {
return nil, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink())
}
}
if len(options.RuleSet) > 0 {
//nolint:staticcheck
if options.Deprecated_RulesetIPCIDRMatchSource {
return nil, E.New("rule_set_ipcidr_match_source is deprecated in sing-box 1.10.0 and removed in sing-box 1.11.0")
}
var matchSource bool
if options.RuleSetIPCIDRMatchSource {
matchSource = true
}
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck
rule.ruleSetItem = item
rule.allItems = append(rule.allItems, item)
}
return rule, nil
}
func (r *DefaultDNSRule) Action() adapter.RuleAction {
return r.action
}
func (r *DefaultDNSRule) WithAddressLimit() bool {
if len(r.destinationIPCIDRItems) > 0 {
return true
}
return r.ruleSetItem != nil && r.ruleSetItem.ContainsDestinationIPCIDRRule()
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
return r.matchForMatch(metadata)
}
func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
if r.matchResponse {
return false
}
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
return r.abstractDefaultRule.Match(metadata)
}
func (r *DefaultDNSRule) MatchResponseTag() string {
return r.matchResponseTag
}
func (r *DefaultDNSRule) MatchResponseTags() []string {
if r.matchResponseTag == "" {
return nil
}
return []string{r.matchResponseTag}
}
func (r *DefaultDNSRule) MatchResponseAnonymous() bool {
return r.matchResponse && r.matchResponseTag == ""
}
func (r *DefaultDNSRule) Race() bool {
return r.race
}
func (r *DefaultDNSRule) matchForMatch(metadata *adapter.InboundContext) bool {
if r.matchResponse {
response := metadata.DNSResponse
if r.matchResponseTag != "" {
response = metadata.NamedDNSResponses[r.matchResponseTag]
}
if response == nil {
return r.invert
}
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractDefaultRule.Match(&matchMetadata)
}
return r.abstractDefaultRule.Match(metadata)
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.ResetRuleCache()
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractDefaultRule.Match(&matchMetadata)
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
type LogicalDNSRule struct {
abstractLogicalRule
matchResponseTags []string
matchResponseAnonymous bool
race bool
}
func (r *LogicalDNSRule) MatchResponseTag() string {
return ""
}
func (r *LogicalDNSRule) MatchResponseTags() []string {
return r.matchResponseTags
}
func (r *LogicalDNSRule) MatchResponseAnonymous() bool {
return r.matchResponseAnonymous
}
func (r *LogicalDNSRule) Race() bool {
return r.race
}
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule, legacyDNSMode bool) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.HeadlessRule, len(options.Rules)),
invert: options.Invert,
action: NewDNSRuleAction(logger, options.DNSRuleAction),
},
race: options.Race,
}
switch options.Mode {
case C.LogicalTypeAnd:
r.mode = C.LogicalTypeAnd
case C.LogicalTypeOr:
r.mode = C.LogicalTypeOr
default:
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
err := validateNoNestedDNSRuleActions(subRule, true)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
rule, err := NewDNSRule(ctx, logger, subRule, false, legacyDNSMode)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
r.rules[i] = rule
}
for _, subRule := range r.rules {
if dnsRule, isDNSRule := subRule.(adapter.DNSRule); isDNSRule {
r.matchResponseTags = append(r.matchResponseTags, dnsRule.MatchResponseTags()...)
r.matchResponseAnonymous = r.matchResponseAnonymous || dnsRule.MatchResponseAnonymous()
}
}
r.matchResponseTags = common.Uniq(r.matchResponseTags)
if r.race && len(r.matchResponseTags) == 0 && !r.matchResponseAnonymous {
return nil, E.New("`race` requires `match_response` in sub-rules")
}
return r, nil
}
func (r *LogicalDNSRule) Action() adapter.RuleAction {
return r.action
}
func (r *LogicalDNSRule) WithAddressLimit() bool {
for _, rawRule := range r.rules {
if dnsRule, isDNSRule := rawRule.(adapter.DNSRule); isDNSRule && dnsRule.WithAddressLimit() {
return true
}
}
return false
}
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
return r.abstractLogicalRule.Match(metadata)
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.ResetRuleCache()
matchMetadata.DNSResponse = response
matchMetadata.DestinationAddressMatchFromResponse = true
return r.abstractLogicalRule.Match(&matchMetadata)
}