dns: use response-only address matching

This commit is contained in:
世界
2026-03-24 20:58:28 +08:00
parent ae65281254
commit 097e75cc02
11 changed files with 229 additions and 51 deletions

View File

@@ -11,6 +11,8 @@ import (
"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, legacyAddressFilter bool) (adapter.DNSRule, error) {
@@ -367,8 +369,11 @@ func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) r
return r.abstractDefaultRule.matchStates(&matchMetadata)
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
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)
@@ -477,6 +482,9 @@ func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStatesForMatch(metadata).isEmpty()
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return !r.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()
}

View File

@@ -77,7 +77,7 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
return r.ipSet.Contains(metadata.Source.Addr)
}
if metadata.DestinationAddressMatchFromResponse {
for _, address := range metadata.DestinationAddressesForMatch() {
for _, address := range metadata.DNSResponseAddressesForMatch() {
if r.ipSet.Contains(address) {
return true
}
@@ -87,7 +87,7 @@ func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
if metadata.Destination.IsIP() {
return r.ipSet.Contains(metadata.Destination.Addr)
}
addresses := metadata.DestinationAddressesForMatch()
addresses := metadata.DestinationAddresses
if len(addresses) > 0 {
for _, address := range addresses {
if r.ipSet.Contains(address) {

View File

@@ -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
}

View File

@@ -20,7 +20,7 @@ func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
return !N.IsPublicAddr(metadata.Source.Addr)
}
if metadata.DestinationAddressMatchFromResponse {
for _, destinationAddress := range metadata.DestinationAddressesForMatch() {
for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() {
if !N.IsPublicAddr(destinationAddress) {
return true
}
@@ -30,7 +30,7 @@ func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
if metadata.Destination.Addr.IsValid() {
return !N.IsPublicAddr(metadata.Destination.Addr)
}
for _, destinationAddress := range metadata.DestinationAddressesForMatch() {
for _, destinationAddress := range metadata.DestinationAddresses {
if !N.IsPublicAddr(destinationAddress) {
return true
}

View File

@@ -583,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()
@@ -598,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()
@@ -612,7 +612,7 @@ func TestDNSRuleSetSemantics(t *testing.T) {
ipCidrAcceptEmpty: true,
})
})
require.True(t, rule.MatchAddressLimit(&metadata))
require.True(t, rule.MatchAddressLimit(&metadata, dnsResponseForTest()))
require.False(t, metadata.IPCIDRMatchSource)
require.False(t, metadata.IPCIDRAcceptEmpty)
})
@@ -639,6 +639,62 @@ func TestDNSMatchResponseRuleSetDestinationCIDRUsesDNSResponse(t *testing.T) {
require.False(t, rule.Match(&unmatchedMetadata))
}
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 TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
t.Parallel()
testCases := []struct {
@@ -688,11 +744,11 @@ 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) {