Add evaluate DNS rule action and related rule items
This commit is contained in:
@@ -70,6 +70,10 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
|
||||
|
||||
func (r *Router) Initialize(rules []option.Rule, ruleSets []option.RuleSet) error {
|
||||
for i, options := range rules {
|
||||
err := R.ValidateNoNestedRuleActions(options)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule[", i, "]")
|
||||
}
|
||||
rule, err := R.NewRule(r.ctx, r.logger, options, false)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse rule[", i, "]")
|
||||
|
||||
@@ -156,7 +156,6 @@ func (r *abstractDefaultRule) matchStatesWithBase(metadata *adapter.InboundConte
|
||||
return r.invertedFailure(inheritedBase)
|
||||
}
|
||||
if r.invert {
|
||||
// DNS pre-lookup defers destination address-limit checks until the response phase.
|
||||
if metadata.IgnoreDestinationIPCIDRMatch && stateSet == emptyRuleMatchState() && !metadata.DidMatch && len(r.destinationIPCIDRItems) > 0 {
|
||||
return emptyRuleMatchState().withBase(inheritedBase)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,18 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
|
||||
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
|
||||
},
|
||||
}
|
||||
case C.RuleActionTypeEvaluate:
|
||||
return &RuleActionEvaluate{
|
||||
Server: action.RouteOptions.Server,
|
||||
RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{
|
||||
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
|
||||
DisableCache: action.RouteOptions.DisableCache,
|
||||
RewriteTTL: action.RouteOptions.RewriteTTL,
|
||||
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
|
||||
},
|
||||
}
|
||||
case C.RuleActionTypeRespond:
|
||||
return &RuleActionRespond{}
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
return &RuleActionDNSRouteOptions{
|
||||
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
|
||||
@@ -230,7 +242,7 @@ func (r *RuleActionRouteOptions) Descriptions() []string {
|
||||
descriptions = append(descriptions, F.ToString("network-type=", strings.Join(common.Map(r.NetworkType, C.InterfaceType.String), ",")))
|
||||
}
|
||||
if r.FallbackNetworkType != nil {
|
||||
descriptions = append(descriptions, F.ToString("fallback-network-type="+strings.Join(common.Map(r.NetworkType, C.InterfaceType.String), ",")))
|
||||
descriptions = append(descriptions, F.ToString("fallback-network-type=", strings.Join(common.Map(r.FallbackNetworkType, C.InterfaceType.String), ",")))
|
||||
}
|
||||
if r.FallbackDelay > 0 {
|
||||
descriptions = append(descriptions, F.ToString("fallback-delay=", r.FallbackDelay.String()))
|
||||
@@ -266,18 +278,45 @@ func (r *RuleActionDNSRoute) Type() string {
|
||||
}
|
||||
|
||||
func (r *RuleActionDNSRoute) String() string {
|
||||
return formatDNSRouteAction("route", r.Server, r.RuleActionDNSRouteOptions)
|
||||
}
|
||||
|
||||
type RuleActionEvaluate struct {
|
||||
Server string
|
||||
RuleActionDNSRouteOptions
|
||||
}
|
||||
|
||||
func (r *RuleActionEvaluate) Type() string {
|
||||
return C.RuleActionTypeEvaluate
|
||||
}
|
||||
|
||||
func (r *RuleActionEvaluate) String() string {
|
||||
return formatDNSRouteAction("evaluate", r.Server, r.RuleActionDNSRouteOptions)
|
||||
}
|
||||
|
||||
type RuleActionRespond struct{}
|
||||
|
||||
func (r *RuleActionRespond) Type() string {
|
||||
return C.RuleActionTypeRespond
|
||||
}
|
||||
|
||||
func (r *RuleActionRespond) String() string {
|
||||
return "respond"
|
||||
}
|
||||
|
||||
func formatDNSRouteAction(action string, server string, options RuleActionDNSRouteOptions) string {
|
||||
var descriptions []string
|
||||
descriptions = append(descriptions, r.Server)
|
||||
if r.DisableCache {
|
||||
descriptions = append(descriptions, server)
|
||||
if options.DisableCache {
|
||||
descriptions = append(descriptions, "disable-cache")
|
||||
}
|
||||
if r.RewriteTTL != nil {
|
||||
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
|
||||
if options.RewriteTTL != nil {
|
||||
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL))
|
||||
}
|
||||
if r.ClientSubnet.IsValid() {
|
||||
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
|
||||
if options.ClientSubnet.IsValid() {
|
||||
descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet))
|
||||
}
|
||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||
return F.ToString(action, "(", strings.Join(descriptions, ","), ")")
|
||||
}
|
||||
|
||||
type RuleActionDNSRouteOptions struct {
|
||||
|
||||
@@ -326,6 +326,10 @@ func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||
}
|
||||
for i, subOptions := range options.Rules {
|
||||
err = validateNoNestedRuleActions(subOptions, true)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
subRule, err := NewRule(ctx, logger, subOptions, false)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "sub rule[", i, "]")
|
||||
|
||||
+158
-25
@@ -5,58 +5,84 @@ import (
|
||||
|
||||
"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) (adapter.DNSRule, error) {
|
||||
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
|
||||
}
|
||||
switch options.DefaultOptions.Action {
|
||||
case "", C.RuleActionTypeRoute:
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
|
||||
return nil, E.New("missing server field")
|
||||
}
|
||||
}
|
||||
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions)
|
||||
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:
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
|
||||
return nil, E.New("missing server field")
|
||||
}
|
||||
}
|
||||
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions)
|
||||
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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
|
||||
|
||||
type DefaultDNSRule struct {
|
||||
abstractDefaultRule
|
||||
matchResponse bool
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
return r.abstractDefaultRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
|
||||
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,
|
||||
}
|
||||
if len(options.Inbound) > 0 {
|
||||
item := NewInboundRule(options.Inbound)
|
||||
@@ -116,7 +142,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.Geosite) > 0 {
|
||||
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 {
|
||||
@@ -151,11 +177,36 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if options.IPAcceptAny {
|
||||
if options.IPAcceptAny { //nolint:staticcheck
|
||||
if legacyDNSMode {
|
||||
deprecated.Report(ctx, deprecated.OptionIPAcceptAny)
|
||||
} else {
|
||||
return nil, E.New(deprecated.OptionIPAcceptAny.MessageWithLink())
|
||||
}
|
||||
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)
|
||||
@@ -275,6 +326,13 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
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 {
|
||||
@@ -284,7 +342,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
if options.RuleSetIPCIDRMatchSource {
|
||||
matchSource = true
|
||||
}
|
||||
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty)
|
||||
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck
|
||||
rule.ruleSetItem = item
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
@@ -309,15 +367,35 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = true
|
||||
defer func() {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = false
|
||||
}()
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
return !r.matchStatesForMatch(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
|
||||
if r.matchResponse {
|
||||
return false
|
||||
}
|
||||
metadata.IgnoreDestinationIPCIDRMatch = true
|
||||
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
|
||||
return !r.abstractDefaultRule.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
if r.matchResponse {
|
||||
if metadata.DNSResponse == nil {
|
||||
return r.abstractDefaultRule.invertedFailure(0)
|
||||
}
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return r.abstractDefaultRule.matchStates(&matchMetadata)
|
||||
}
|
||||
return r.abstractDefaultRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.DNSResponse = response
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty()
|
||||
}
|
||||
|
||||
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
|
||||
@@ -330,7 +408,53 @@ func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatch
|
||||
return r.abstractLogicalRule.matchStates(metadata)
|
||||
}
|
||||
|
||||
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
|
||||
func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
switch typedRule := rule.(type) {
|
||||
case *DefaultDNSRule:
|
||||
return typedRule.matchStatesForMatch(metadata)
|
||||
case *LogicalDNSRule:
|
||||
return typedRule.matchStatesForMatch(metadata)
|
||||
default:
|
||||
return matchHeadlessRuleStates(typedRule, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
|
||||
var stateSet ruleMatchStateSet
|
||||
if r.mode == C.LogicalTypeAnd {
|
||||
stateSet = emptyRuleMatchState()
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
nestedStateSet := matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata)
|
||||
if nestedStateSet.isEmpty() {
|
||||
if r.invert {
|
||||
return emptyRuleMatchState()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
stateSet = stateSet.combine(nestedStateSet)
|
||||
}
|
||||
} else {
|
||||
for _, rule := range r.rules {
|
||||
nestedMetadata := *metadata
|
||||
nestedMetadata.ResetRuleCache()
|
||||
stateSet = stateSet.merge(matchDNSHeadlessRuleStatesForMatch(rule, &nestedMetadata))
|
||||
}
|
||||
if stateSet.isEmpty() {
|
||||
if r.invert {
|
||||
return emptyRuleMatchState()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
}
|
||||
if r.invert {
|
||||
return 0
|
||||
}
|
||||
return stateSet
|
||||
}
|
||||
|
||||
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)),
|
||||
@@ -347,7 +471,11 @@ func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
return nil, E.New("unknown logical mode: ", options.Mode)
|
||||
}
|
||||
for i, subRule := range options.Rules {
|
||||
rule, err := NewDNSRule(ctx, logger, subRule, false)
|
||||
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, "]")
|
||||
}
|
||||
@@ -377,13 +505,18 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = true
|
||||
defer func() {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = false
|
||||
}()
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
return !r.matchStatesForMatch(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
|
||||
metadata.IgnoreDestinationIPCIDRMatch = true
|
||||
defer func() { metadata.IgnoreDestinationIPCIDRMatch = false }()
|
||||
return !r.abstractLogicalRule.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
|
||||
matchMetadata := *metadata
|
||||
matchMetadata.DNSResponse = response
|
||||
matchMetadata.DestinationAddressMatchFromResponse = true
|
||||
return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty()
|
||||
}
|
||||
|
||||
@@ -76,11 +76,26 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if r.isSource || metadata.IPCIDRMatchSource {
|
||||
return r.ipSet.Contains(metadata.Source.Addr)
|
||||
}
|
||||
if metadata.DestinationAddressMatchFromResponse {
|
||||
addresses := metadata.DNSResponseAddressesForMatch()
|
||||
if len(addresses) == 0 {
|
||||
// Legacy rule_set_ip_cidr_accept_empty only applies when the DNS response
|
||||
// does not expose any address answers for matching.
|
||||
return metadata.IPCIDRAcceptEmpty
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if r.ipSet.Contains(address) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if metadata.Destination.IsIP() {
|
||||
return r.ipSet.Contains(metadata.Destination.Addr)
|
||||
}
|
||||
if len(metadata.DestinationAddresses) > 0 {
|
||||
for _, address := range metadata.DestinationAddresses {
|
||||
addresses := metadata.DestinationAddresses
|
||||
if len(addresses) > 0 {
|
||||
for _, address := range addresses {
|
||||
if r.ipSet.Contains(address) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ func NewIPAcceptAnyItem() *IPAcceptAnyItem {
|
||||
}
|
||||
|
||||
func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.DestinationAddressMatchFromResponse {
|
||||
return len(metadata.DNSResponseAddressesForMatch()) > 0
|
||||
}
|
||||
return len(metadata.DestinationAddresses) > 0
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
@@ -18,21 +16,24 @@ func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem {
|
||||
}
|
||||
|
||||
func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
|
||||
var destination netip.Addr
|
||||
if r.isSource {
|
||||
destination = metadata.Source.Addr
|
||||
} else {
|
||||
destination = metadata.Destination.Addr
|
||||
return !N.IsPublicAddr(metadata.Source.Addr)
|
||||
}
|
||||
if destination.IsValid() {
|
||||
return !N.IsPublicAddr(destination)
|
||||
}
|
||||
if !r.isSource {
|
||||
for _, destinationAddress := range metadata.DestinationAddresses {
|
||||
if metadata.DestinationAddressMatchFromResponse {
|
||||
for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() {
|
||||
if !N.IsPublicAddr(destinationAddress) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if metadata.Destination.Addr.IsValid() {
|
||||
return !N.IsPublicAddr(metadata.Destination.Addr)
|
||||
}
|
||||
for _, destinationAddress := range metadata.DestinationAddresses {
|
||||
if !N.IsPublicAddr(destinationAddress) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*DNSResponseRCodeItem)(nil)
|
||||
|
||||
type DNSResponseRCodeItem struct {
|
||||
rcode int
|
||||
}
|
||||
|
||||
func NewDNSResponseRCodeItem(rcode int) *DNSResponseRCodeItem {
|
||||
return &DNSResponseRCodeItem{rcode: rcode}
|
||||
}
|
||||
|
||||
func (r *DNSResponseRCodeItem) Match(metadata *adapter.InboundContext) bool {
|
||||
return metadata.DNSResponse != nil && metadata.DNSResponse.Rcode == r.rcode
|
||||
}
|
||||
|
||||
func (r *DNSResponseRCodeItem) String() string {
|
||||
return F.ToString("response_rcode=", dns.RcodeToString[r.rcode])
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*DNSResponseRecordItem)(nil)
|
||||
|
||||
type DNSResponseRecordItem struct {
|
||||
field string
|
||||
records []option.DNSRecordOptions
|
||||
selector func(*dns.Msg) []dns.RR
|
||||
}
|
||||
|
||||
func NewDNSResponseRecordItem(field string, records []option.DNSRecordOptions, selector func(*dns.Msg) []dns.RR) *DNSResponseRecordItem {
|
||||
return &DNSResponseRecordItem{
|
||||
field: field,
|
||||
records: records,
|
||||
selector: selector,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *DNSResponseRecordItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.DNSResponse == nil {
|
||||
return false
|
||||
}
|
||||
records := r.selector(metadata.DNSResponse)
|
||||
for _, expected := range r.records {
|
||||
for _, record := range records {
|
||||
if expected.Match(record) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *DNSResponseRecordItem) String() string {
|
||||
descriptions := make([]string, 0, len(r.records))
|
||||
for _, record := range r.records {
|
||||
if record.RR != nil {
|
||||
descriptions = append(descriptions, record.RR.String())
|
||||
}
|
||||
}
|
||||
return r.field + "=[" + strings.Join(descriptions, " ") + "]"
|
||||
}
|
||||
|
||||
func dnsResponseAnswers(message *dns.Msg) []dns.RR {
|
||||
return message.Answer
|
||||
}
|
||||
|
||||
func dnsResponseNS(message *dns.Msg) []dns.RR {
|
||||
return message.Ns
|
||||
}
|
||||
|
||||
func dnsResponseExtra(message *dns.Msg) []dns.RR {
|
||||
return message.Extra
|
||||
}
|
||||
@@ -29,9 +29,11 @@ func NewRuleSetItem(router adapter.Router, tagList []string, ipCIDRMatchSource b
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Start() error {
|
||||
_ = r.Close()
|
||||
for _, tag := range r.tagList {
|
||||
ruleSet, loaded := r.router.RuleSet(tag)
|
||||
if !loaded {
|
||||
_ = r.Close()
|
||||
return E.New("rule-set not found: ", tag)
|
||||
}
|
||||
ruleSet.IncRef()
|
||||
@@ -40,6 +42,15 @@ func (r *RuleSetItem) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Close() error {
|
||||
for _, ruleSet := range r.setList {
|
||||
ruleSet.DecRef()
|
||||
}
|
||||
clear(r.setList)
|
||||
r.setList = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
|
||||
return !r.matchStates(metadata).isEmpty()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go4.org/netipx"
|
||||
)
|
||||
|
||||
type ruleSetItemTestRouter struct {
|
||||
ruleSets map[string]adapter.RuleSet
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil }
|
||||
func (r *ruleSetItemTestRouter) Close() error { return nil }
|
||||
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) RoutePacketConnection(context.Context, N.PacketConn, adapter.InboundContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) RouteConnectionEx(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) {
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) RoutePacketConnectionEx(context.Context, N.PacketConn, adapter.InboundContext, N.CloseHandlerFunc) {
|
||||
}
|
||||
|
||||
func (r *ruleSetItemTestRouter) RuleSet(tag string) (adapter.RuleSet, bool) {
|
||||
ruleSet, loaded := r.ruleSets[tag]
|
||||
return ruleSet, loaded
|
||||
}
|
||||
func (r *ruleSetItemTestRouter) Rules() []adapter.Rule { return nil }
|
||||
func (r *ruleSetItemTestRouter) NeedFindProcess() bool { return false }
|
||||
func (r *ruleSetItemTestRouter) NeedFindNeighbor() bool { return false }
|
||||
func (r *ruleSetItemTestRouter) NeighborResolver() adapter.NeighborResolver { return nil }
|
||||
func (r *ruleSetItemTestRouter) AppendTracker(adapter.ConnectionTracker) {}
|
||||
func (r *ruleSetItemTestRouter) ResetNetwork() {}
|
||||
|
||||
type countingRuleSet struct {
|
||||
name string
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func (s *countingRuleSet) Name() string { return s.name }
|
||||
func (s *countingRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error { return nil }
|
||||
func (s *countingRuleSet) PostStart() error { return nil }
|
||||
func (s *countingRuleSet) Metadata() adapter.RuleSetMetadata { return adapter.RuleSetMetadata{} }
|
||||
func (s *countingRuleSet) ExtractIPSet() []*netipx.IPSet { return nil }
|
||||
func (s *countingRuleSet) IncRef() { s.refs.Add(1) }
|
||||
func (s *countingRuleSet) DecRef() {
|
||||
if s.refs.Add(-1) < 0 {
|
||||
panic("rule-set: negative refs")
|
||||
}
|
||||
}
|
||||
func (s *countingRuleSet) Cleanup() {}
|
||||
func (s *countingRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||
return nil
|
||||
}
|
||||
func (s *countingRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}
|
||||
func (s *countingRuleSet) Close() error { return nil }
|
||||
func (s *countingRuleSet) Match(*adapter.InboundContext) bool { return true }
|
||||
func (s *countingRuleSet) String() string { return s.name }
|
||||
func (s *countingRuleSet) RefCount() int32 { return s.refs.Load() }
|
||||
|
||||
func TestRuleSetItemCloseReleasesRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
firstSet := &countingRuleSet{name: "first"}
|
||||
secondSet := &countingRuleSet{name: "second"}
|
||||
item := NewRuleSetItem(&ruleSetItemTestRouter{
|
||||
ruleSets: map[string]adapter.RuleSet{
|
||||
"first": firstSet,
|
||||
"second": secondSet,
|
||||
},
|
||||
}, []string{"first", "second"}, false, false)
|
||||
|
||||
require.NoError(t, item.Start())
|
||||
require.EqualValues(t, 1, firstSet.RefCount())
|
||||
require.EqualValues(t, 1, secondSet.RefCount())
|
||||
|
||||
require.NoError(t, item.Close())
|
||||
require.Zero(t, firstSet.RefCount())
|
||||
require.Zero(t, secondSet.RefCount())
|
||||
|
||||
require.NoError(t, item.Close())
|
||||
require.Zero(t, firstSet.RefCount())
|
||||
require.Zero(t, secondSet.RefCount())
|
||||
}
|
||||
|
||||
func TestRuleSetItemStartRollbackOnFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
firstSet := &countingRuleSet{name: "first"}
|
||||
item := NewRuleSetItem(&ruleSetItemTestRouter{
|
||||
ruleSets: map[string]adapter.RuleSet{
|
||||
"first": firstSet,
|
||||
},
|
||||
}, []string{"first", "missing"}, false, false)
|
||||
|
||||
err := item.Start()
|
||||
require.ErrorContains(t, err, "rule-set not found: missing")
|
||||
require.Zero(t, firstSet.RefCount())
|
||||
}
|
||||
|
||||
func TestRuleSetItemRestartKeepsBalancedRefs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
firstSet := &countingRuleSet{name: "first"}
|
||||
item := NewRuleSetItem(&ruleSetItemTestRouter{
|
||||
ruleSets: map[string]adapter.RuleSet{
|
||||
"first": firstSet,
|
||||
},
|
||||
}, []string{"first"}, false, false)
|
||||
|
||||
require.NoError(t, item.Start())
|
||||
require.EqualValues(t, 1, firstSet.RefCount())
|
||||
|
||||
require.NoError(t, item.Start())
|
||||
require.EqualValues(t, 1, firstSet.RefCount())
|
||||
|
||||
require.NoError(t, item.Close())
|
||||
require.Zero(t, firstSet.RefCount())
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func ValidateNoNestedRuleActions(rule option.Rule) error {
|
||||
return validateNoNestedRuleActions(rule, false)
|
||||
}
|
||||
|
||||
func ValidateNoNestedDNSRuleActions(rule option.DNSRule) error {
|
||||
return validateNoNestedDNSRuleActions(rule, false)
|
||||
}
|
||||
|
||||
func validateNoNestedRuleActions(rule option.Rule, nested bool) error {
|
||||
if nested && ruleHasConfiguredAction(rule) {
|
||||
return E.New(option.RouteRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
if rule.Type != C.RuleTypeLogical {
|
||||
return nil
|
||||
}
|
||||
for i, subRule := range rule.LogicalOptions.Rules {
|
||||
err := validateNoNestedRuleActions(subRule, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNoNestedDNSRuleActions(rule option.DNSRule, nested bool) error {
|
||||
if nested && dnsRuleHasConfiguredAction(rule) {
|
||||
return E.New(option.DNSRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
if rule.Type != C.RuleTypeLogical {
|
||||
return nil
|
||||
}
|
||||
for i, subRule := range rule.LogicalOptions.Rules {
|
||||
err := validateNoNestedDNSRuleActions(subRule, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ruleHasConfiguredAction(rule option.Rule) bool {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
return !reflect.DeepEqual(rule.DefaultOptions.RuleAction, option.RuleAction{})
|
||||
case C.RuleTypeLogical:
|
||||
return !reflect.DeepEqual(rule.LogicalOptions.RuleAction, option.RuleAction{})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRuleHasConfiguredAction(rule option.DNSRule) bool {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
return !reflect.DeepEqual(rule.DefaultOptions.DNSRuleAction, option.DNSRuleAction{})
|
||||
case C.RuleTypeLogical:
|
||||
return !reflect.DeepEqual(rule.LogicalOptions.DNSRuleAction, option.DNSRuleAction{})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRuleRejectsNestedRuleAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), option.Rule{
|
||||
Type: C.RuleTypeLogical,
|
||||
LogicalOptions: option.LogicalRule{
|
||||
RawLogicalRule: option.RawLogicalRule{
|
||||
Mode: C.LogicalTypeAnd,
|
||||
Rules: []option.Rule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultRule{
|
||||
RuleAction: option.RuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.RouteActionOptions{
|
||||
Outbound: "direct",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}, false)
|
||||
require.ErrorContains(t, err, option.RouteRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestNewDNSRuleRejectsNestedRuleAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{
|
||||
Type: C.RuleTypeLogical,
|
||||
LogicalOptions: option.LogicalDNSRule{
|
||||
RawLogicalDNSRule: option.RawLogicalDNSRule{
|
||||
Mode: C.LogicalTypeAnd,
|
||||
Rules: []option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{
|
||||
Server: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{
|
||||
Server: "default",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, true, false)
|
||||
require.ErrorContains(t, err, option.DNSRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestNewDNSRuleRejectsReplyRejectMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), option.DNSRule{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
Domain: []string{"example.com"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeReject,
|
||||
RejectOptions: option.RejectActionOptions{
|
||||
Method: C.RuleActionRejectMethodReply,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, false, false)
|
||||
require.ErrorContains(t, err, "reject method `reply` is not supported for DNS rules")
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"go4.org/netipx"
|
||||
)
|
||||
@@ -69,3 +70,24 @@ func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||
func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.IPCIDR) > 0 || rule.IPSet != nil
|
||||
}
|
||||
|
||||
func isDNSQueryTypeHeadlessRule(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.QueryType) > 0
|
||||
}
|
||||
|
||||
func buildRuleSetMetadata(headlessRules []option.HeadlessRule) adapter.RuleSetMetadata {
|
||||
return adapter.RuleSetMetadata{
|
||||
ContainsProcessRule: HasHeadlessRule(headlessRules, isProcessHeadlessRule),
|
||||
ContainsWIFIRule: HasHeadlessRule(headlessRules, isWIFIHeadlessRule),
|
||||
ContainsIPCIDRRule: HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule),
|
||||
ContainsDNSQueryTypeRule: HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule),
|
||||
}
|
||||
}
|
||||
|
||||
func validateRuleSetMetadataUpdate(ctx context.Context, tag string, metadata adapter.RuleSetMetadata) error {
|
||||
validator := service.FromContext[adapter.DNSRuleSetUpdateValidator](ctx)
|
||||
if validator == nil {
|
||||
return nil
|
||||
}
|
||||
return validator.ValidateRuleSetMetadataUpdate(tag, metadata)
|
||||
}
|
||||
|
||||
@@ -137,10 +137,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
var metadata adapter.RuleSetMetadata
|
||||
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
|
||||
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
|
||||
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
|
||||
metadata := buildRuleSetMetadata(headlessRules)
|
||||
err = validateRuleSetMetadataUpdate(s.ctx, s.tag, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.access.Lock()
|
||||
s.rules = rules
|
||||
s.metadata = metadata
|
||||
|
||||
@@ -189,10 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
metadata := buildRuleSetMetadata(plainRuleSet.Rules)
|
||||
err = validateRuleSetMetadataUpdate(s.ctx, s.options.Tag, metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.access.Lock()
|
||||
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
|
||||
s.metadata = metadata
|
||||
s.rules = rules
|
||||
callbacks := s.callbacks.Array()
|
||||
s.access.Unlock()
|
||||
|
||||
@@ -2,6 +2,7 @@ package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -581,7 +583,7 @@ func TestDNSRuleSetSemantics(t *testing.T) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
|
||||
})
|
||||
t.Run("dns keeps ruleset or semantics", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -596,7 +598,7 @@ func TestDNSRuleSetSemantics(t *testing.T) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{emptyStateSet, destinationStateSet}})
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
|
||||
})
|
||||
t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -610,10 +612,384 @@ func TestDNSRuleSetSemantics(t *testing.T) {
|
||||
ipCidrAcceptEmpty: true,
|
||||
})
|
||||
})
|
||||
require.True(t, rule.MatchAddressLimit(&metadata))
|
||||
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
|
||||
require.False(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
|
||||
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest()))
|
||||
require.False(t, metadata.IPCIDRMatchSource)
|
||||
require.False(t, metadata.IPCIDRAcceptEmpty)
|
||||
})
|
||||
t.Run("pre lookup ruleset only deferred fields fail closed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
ruleSet := newLocalRuleSetForTest("dns-prelookup-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
// This is accepted without match_response so mixed rule_set deployments keep
|
||||
// working; the destination-IP-only branch simply cannot match before a DNS
|
||||
// response is available.
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("pre lookup ruleset destination cidr does not fall back to other predicates", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
ruleSet := newLocalRuleSetForTest("dns-prelookup-network-and-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("pre lookup mixed ruleset still matches non response branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("www.example.com")
|
||||
ruleSet := newLocalRuleSetForTest(
|
||||
"dns-prelookup-mixed",
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}),
|
||||
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationAddressItem(t, rule, nil, []string{"example.com"})
|
||||
}),
|
||||
)
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
// Destination-IP predicates inside rule_set fail closed before the DNS response,
|
||||
// but they must not force validation errors or suppress sibling non-response
|
||||
// branches.
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDNSMatchResponseRuleSetDestinationCIDRUsesDNSResponse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ruleSet := newLocalRuleSetForTest("dns-response-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
rule.matchResponse = true
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
matchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))
|
||||
require.True(t, rule.Match(&matchedMetadata))
|
||||
require.Empty(t, matchedMetadata.DestinationAddresses)
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
unmatchedMetadata.DNSResponse = dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))
|
||||
require.False(t, rule.Match(&unmatchedMetadata))
|
||||
}
|
||||
|
||||
func TestDNSMatchResponseMissingResponseUsesBooleanSemantics(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("plain rule remains false", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {})
|
||||
rule.matchResponse = true
|
||||
|
||||
metadata := testMetadata("lookup.example")
|
||||
require.False(t, rule.Match(&metadata))
|
||||
})
|
||||
|
||||
t.Run("invert rule becomes true", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
})
|
||||
rule.matchResponse = true
|
||||
|
||||
metadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
|
||||
t.Run("logical wrapper respects inverted child", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
})
|
||||
nestedRule.matchResponse = true
|
||||
|
||||
logicalRule := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: []adapter.HeadlessRule{nestedRule},
|
||||
mode: C.LogicalTypeAnd,
|
||||
},
|
||||
}
|
||||
|
||||
metadata := testMetadata("lookup.example")
|
||||
require.True(t, logicalRule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDNSAddressLimitIgnoresDestinationAddresses(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
build func(*testing.T, *abstractDefaultRule)
|
||||
matchedResponse *mDNS.Msg
|
||||
unmatchedResponse *mDNS.Msg
|
||||
}{
|
||||
{
|
||||
name: "ip_cidr",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")),
|
||||
unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")),
|
||||
},
|
||||
{
|
||||
name: "ip_is_private",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")),
|
||||
unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")),
|
||||
},
|
||||
{
|
||||
name: "ip_accept_any",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPAcceptAnyItem(rule)
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")),
|
||||
unmatchedResponse: dnsResponseForTest(),
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
testCase.build(t, rule)
|
||||
})
|
||||
|
||||
mismatchMetadata := testMetadata("lookup.example")
|
||||
mismatchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("203.0.113.1")}
|
||||
require.False(t, rule.MatchAddressLimit(&mismatchMetadata, testCase.unmatchedResponse))
|
||||
|
||||
matchMetadata := testMetadata("lookup.example")
|
||||
matchMetadata.DestinationAddresses = []netip.Addr{netip.MustParseAddr("8.8.8.8")}
|
||||
require.True(t, rule.MatchAddressLimit(&matchMetadata, testCase.matchedResponse))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSLegacyAddressLimitPreLookupDefersDirectRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
build func(*testing.T, *abstractDefaultRule)
|
||||
matchedResponse *mDNS.Msg
|
||||
unmatchedResponse *mDNS.Msg
|
||||
}{
|
||||
{
|
||||
name: "ip_cidr",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")),
|
||||
unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")),
|
||||
},
|
||||
{
|
||||
name: "ip_is_private",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("10.0.0.1")),
|
||||
unmatchedResponse: dnsResponseForTest(netip.MustParseAddr("8.8.8.8")),
|
||||
},
|
||||
{
|
||||
name: "ip_accept_any",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPAcceptAnyItem(rule)
|
||||
},
|
||||
matchedResponse: dnsResponseForTest(netip.MustParseAddr("203.0.113.1")),
|
||||
unmatchedResponse: dnsResponseForTest(),
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
testCase.build(t, rule)
|
||||
})
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.LegacyPreMatch(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.MatchAddressLimit(&matchedMetadata, testCase.matchedResponse))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, testCase.unmatchedResponse))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSLegacyAddressLimitPreLookupDefersRuleSetDestinationCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ruleSet := newLocalRuleSetForTest("dns-legacy-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.LegacyPreMatch(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
require.False(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
|
||||
}
|
||||
|
||||
func TestDNSLegacyLogicalAddressLimitPreLookupDefersNestedRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
})
|
||||
logicalRule := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: []adapter.HeadlessRule{nestedRule},
|
||||
mode: C.LogicalTypeAnd,
|
||||
},
|
||||
}
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
require.True(t, logicalRule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("10.0.0.1"))))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
require.False(t, logicalRule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
|
||||
}
|
||||
|
||||
func TestDNSLegacyInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
build func(*testing.T, *abstractDefaultRule)
|
||||
matchedAddrs []netip.Addr
|
||||
unmatchedAddrs []netip.Addr
|
||||
}{
|
||||
{
|
||||
name: "ip_cidr",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")},
|
||||
unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||
},
|
||||
{
|
||||
name: "ip_is_private",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.1")},
|
||||
unmatchedAddrs: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||
},
|
||||
{
|
||||
name: "ip_accept_any",
|
||||
build: func(t *testing.T, rule *abstractDefaultRule) {
|
||||
t.Helper()
|
||||
addDestinationIPAcceptAnyItem(rule)
|
||||
},
|
||||
matchedAddrs: []netip.Addr{netip.MustParseAddr("203.0.113.1")},
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
testCase.build(t, rule)
|
||||
})
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.LegacyPreMatch(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...)))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSLegacyInvertLogicalAddressLimitPreLookupRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("inverted deferred child does not suppress branch", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
logicalRule := &LogicalDNSRule{
|
||||
abstractLogicalRule: abstractLogicalRule{
|
||||
rules: []adapter.HeadlessRule{
|
||||
dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addDestinationIPIsPrivateItem(rule)
|
||||
}),
|
||||
},
|
||||
mode: C.LogicalTypeAnd,
|
||||
},
|
||||
}
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDNSLegacyInvertRuleSetAddressLimitPreLookupRegression(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ruleSet := newLocalRuleSetForTest("dns-legacy-invert-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
rule.invert = true
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
}))
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
|
||||
preLookupMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.LegacyPreMatch(&preLookupMetadata))
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
|
||||
}
|
||||
|
||||
func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
@@ -665,14 +1041,14 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
|
||||
matchedMetadata := testMetadata("lookup.example")
|
||||
matchedMetadata.DestinationAddresses = testCase.matchedAddrs
|
||||
require.False(t, rule.MatchAddressLimit(&matchedMetadata))
|
||||
require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...)))
|
||||
|
||||
unmatchedMetadata := testMetadata("lookup.example")
|
||||
unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs
|
||||
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata))
|
||||
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...)))
|
||||
})
|
||||
}
|
||||
t.Run("mixed resolved and deferred fields keep old pre lookup false", func(t *testing.T) {
|
||||
t.Run("mixed resolved and deferred fields invert matches pre lookup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
|
||||
@@ -680,9 +1056,9 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
|
||||
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
t.Run("ruleset only deferred fields keep old pre lookup false", func(t *testing.T) {
|
||||
t.Run("ruleset only deferred fields invert matches pre lookup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
metadata := testMetadata("lookup.example")
|
||||
ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
|
||||
@@ -692,7 +1068,7 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
|
||||
rule.invert = true
|
||||
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
|
||||
})
|
||||
require.False(t, rule.Match(&metadata))
|
||||
require.True(t, rule.Match(&metadata))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -763,6 +1139,39 @@ func testMetadata(domain string) adapter.InboundContext {
|
||||
}
|
||||
}
|
||||
|
||||
func dnsResponseForTest(addresses ...netip.Addr) *mDNS.Msg {
|
||||
response := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: mDNS.RcodeSuccess,
|
||||
},
|
||||
}
|
||||
for _, address := range addresses {
|
||||
if address.Is4() {
|
||||
response.Answer = append(response.Answer, &mDNS.A{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: mDNS.Fqdn("lookup.example"),
|
||||
Rrtype: mDNS.TypeA,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
A: net.IP(append([]byte(nil), address.AsSlice()...)),
|
||||
})
|
||||
} else {
|
||||
response.Answer = append(response.Answer, &mDNS.AAAA{
|
||||
Hdr: mDNS.RR_Header{
|
||||
Name: mDNS.Fqdn("lookup.example"),
|
||||
Rrtype: mDNS.TypeAAAA,
|
||||
Class: mDNS.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
AAAA: net.IP(append([]byte(nil), address.AsSlice()...)),
|
||||
})
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) {
|
||||
rule.ruleSetItem = item
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeDNSRuleSetUpdateValidator struct {
|
||||
validate func(tag string, metadata adapter.RuleSetMetadata) error
|
||||
}
|
||||
|
||||
func (v *fakeDNSRuleSetUpdateValidator) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error {
|
||||
if v.validate == nil {
|
||||
return nil
|
||||
}
|
||||
return v.validate(tag, metadata)
|
||||
}
|
||||
|
||||
func TestLocalRuleSetReloadRulesRejectsInvalidUpdateBeforeCommit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callbackCount atomic.Int32
|
||||
ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{
|
||||
validate: func(tag string, metadata adapter.RuleSetMetadata) error {
|
||||
require.Equal(t, "dynamic-set", tag)
|
||||
if metadata.ContainsDNSQueryTypeRule {
|
||||
return E.New("dns conflict")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
ruleSet := &LocalRuleSet{
|
||||
ctx: ctx,
|
||||
tag: "dynamic-set",
|
||||
fileFormat: C.RuleSetFormatSource,
|
||||
}
|
||||
_ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) {
|
||||
callbackCount.Add(1)
|
||||
})
|
||||
|
||||
err := ruleSet.reloadRules([]option.HeadlessRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultHeadlessRule{
|
||||
Domain: badoption.Listable[string]{"example.com"},
|
||||
},
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), callbackCount.Load())
|
||||
require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule)
|
||||
require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"}))
|
||||
|
||||
err = ruleSet.reloadRules([]option.HeadlessRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultHeadlessRule{
|
||||
QueryType: badoption.Listable[option.DNSQueryType]{option.DNSQueryType(1)},
|
||||
},
|
||||
}})
|
||||
require.ErrorContains(t, err, "dns conflict")
|
||||
require.Equal(t, int32(1), callbackCount.Load())
|
||||
require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule)
|
||||
require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"}))
|
||||
}
|
||||
|
||||
func TestRemoteRuleSetLoadBytesRejectsInvalidUpdateBeforeCommit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var callbackCount atomic.Int32
|
||||
ctx := service.ContextWith[adapter.DNSRuleSetUpdateValidator](context.Background(), &fakeDNSRuleSetUpdateValidator{
|
||||
validate: func(tag string, metadata adapter.RuleSetMetadata) error {
|
||||
require.Equal(t, "dynamic-set", tag)
|
||||
if metadata.ContainsDNSQueryTypeRule {
|
||||
return E.New("dns conflict")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
ruleSet := &RemoteRuleSet{
|
||||
ctx: ctx,
|
||||
options: option.RuleSet{
|
||||
Tag: "dynamic-set",
|
||||
Format: C.RuleSetFormatSource,
|
||||
},
|
||||
callbacks: list.List[adapter.RuleSetUpdateCallback]{},
|
||||
}
|
||||
_ = ruleSet.callbacks.PushBack(func(adapter.RuleSet) {
|
||||
callbackCount.Add(1)
|
||||
})
|
||||
|
||||
err := ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"domain":["example.com"]}]}`))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), callbackCount.Load())
|
||||
require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule)
|
||||
require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"}))
|
||||
|
||||
err = ruleSet.loadBytes([]byte(`{"version":4,"rules":[{"query_type":["A"]}]}`))
|
||||
require.ErrorContains(t, err, "dns conflict")
|
||||
require.Equal(t, int32(1), callbackCount.Load())
|
||||
require.False(t, ruleSet.metadata.ContainsDNSQueryTypeRule)
|
||||
require.True(t, ruleSet.Match(&adapter.InboundContext{Domain: "example.com"}))
|
||||
}
|
||||
Reference in New Issue
Block a user