mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Add evaluate DNS rule action and related rule items
This commit is contained in:
@@ -25,8 +25,8 @@ type DNSRouter interface {
|
||||
|
||||
type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
@@ -72,11 +72,6 @@ type DNSTransport interface {
|
||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||
}
|
||||
|
||||
type LegacyDNSTransport interface {
|
||||
LegacyStrategy() C.DomainStrategy
|
||||
LegacyClientSubnet() netip.Prefix
|
||||
}
|
||||
|
||||
type DNSTransportRegistry interface {
|
||||
option.DNSTransportOptionsRegistry
|
||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Inbound interface {
|
||||
@@ -79,14 +81,16 @@ type InboundContext struct {
|
||||
FallbackNetworkType []C.InterfaceType
|
||||
FallbackDelay time.Duration
|
||||
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
DestinationAddresses []netip.Addr
|
||||
DNSResponse *dns.Msg
|
||||
DestinationAddressMatchFromResponse bool
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
// rule cache
|
||||
|
||||
@@ -115,6 +119,51 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.DidMatch = false
|
||||
}
|
||||
|
||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
||||
return DNSResponseAddresses(c.DNSResponse)
|
||||
}
|
||||
|
||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawRecord := range response.Answer {
|
||||
switch record := rawRecord.(type) {
|
||||
case *dns.A:
|
||||
addr := M.AddrFromIP(record.A)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.AAAA:
|
||||
addr := M.AddrFromIP(record.AAAA)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
case *dns.HTTPS:
|
||||
for _, value := range record.SVCB.Value {
|
||||
switch hint := value.(type) {
|
||||
case *dns.SVCBIPv4Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip).Unmap()
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
case *dns.SVCBIPv6Hint:
|
||||
for _, ip := range hint.Hint {
|
||||
addr := M.AddrFromIP(ip)
|
||||
if addr.IsValid() {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type inboundContextKey struct{}
|
||||
|
||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||
|
||||
45
adapter/inbound_test.go
Normal file
45
adapter/inbound_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
||||
require.NotNil(t, ipv4Hint)
|
||||
|
||||
response := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.HTTPS{
|
||||
SVCB: dns.SVCB{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: dns.Fqdn("example.com"),
|
||||
Rrtype: dns.TypeHTTPS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 60,
|
||||
},
|
||||
Priority: 1,
|
||||
Target: ".",
|
||||
Value: []dns.SVCBKeyValue{
|
||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
addresses := DNSResponseAddresses(response)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
||||
require.True(t, addresses[0].Is4())
|
||||
}
|
||||
@@ -66,10 +66,16 @@ type RuleSet interface {
|
||||
|
||||
type RuleSetUpdateCallback func(it RuleSet)
|
||||
|
||||
type DNSRuleSetUpdateValidator interface {
|
||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
||||
}
|
||||
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
type RuleSetMetadata struct {
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsProcessRule bool
|
||||
ContainsWIFIRule bool
|
||||
ContainsIPCIDRRule bool
|
||||
ContainsDNSQueryTypeRule bool
|
||||
}
|
||||
type HTTPStartContext struct {
|
||||
ctx context.Context
|
||||
|
||||
@@ -2,6 +2,8 @@ package adapter
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type HeadlessRule interface {
|
||||
@@ -18,8 +20,9 @@ type Rule interface {
|
||||
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
LegacyPreMatch(metadata *InboundContext) bool
|
||||
WithAddressLimit() bool
|
||||
MatchAddressLimit(metadata *InboundContext) bool
|
||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
||||
}
|
||||
|
||||
type RuleAction interface {
|
||||
@@ -29,7 +32,7 @@ type RuleAction interface {
|
||||
|
||||
func IsFinalAction(action RuleAction) bool {
|
||||
switch action.Type() {
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
||||
3
box.go
3
box.go
@@ -195,6 +195,7 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
@@ -483,7 +484,7 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,19 +15,18 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeLegacyRcode = "legacy_rcode"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
DNSTypeLegacy = "legacy"
|
||||
DNSTypeUDP = "udp"
|
||||
DNSTypeTCP = "tcp"
|
||||
DNSTypeTLS = "tls"
|
||||
DNSTypeHTTPS = "https"
|
||||
DNSTypeQUIC = "quic"
|
||||
DNSTypeHTTP3 = "h3"
|
||||
DNSTypeLocal = "local"
|
||||
DNSTypeHosts = "hosts"
|
||||
DNSTypeFakeIP = "fakeip"
|
||||
DNSTypeDHCP = "dhcp"
|
||||
DNSTypeTailscale = "tailscale"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -29,6 +29,8 @@ const (
|
||||
const (
|
||||
RuleActionTypeRoute = "route"
|
||||
RuleActionTypeRouteOptions = "route-options"
|
||||
RuleActionTypeEvaluate = "evaluate"
|
||||
RuleActionTypeRespond = "respond"
|
||||
RuleActionTypeDirect = "direct"
|
||||
RuleActionTypeBypass = "bypass"
|
||||
RuleActionTypeReject = "reject"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"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"
|
||||
@@ -109,7 +107,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
||||
if len(message.Question) == 0 {
|
||||
if c.logger != nil {
|
||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||
@@ -239,13 +237,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
// TODO: add accept_any rule and support to check response instead of addresses
|
||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||
rejected = true
|
||||
} else if len(response.Answer) == 0 {
|
||||
rejected = !responseChecker(nil)
|
||||
} else {
|
||||
rejected = !responseChecker(MessageToAddresses(response))
|
||||
rejected = !responseChecker(response)
|
||||
}
|
||||
if rejected {
|
||||
if !disableCache && c.rdrc != nil {
|
||||
@@ -321,7 +316,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
domain = FqdnToDomain(domain)
|
||||
dnsName := dns.Fqdn(domain)
|
||||
var strategy C.DomainStrategy
|
||||
@@ -406,7 +401,7 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
||||
question := dns.Question{
|
||||
Name: name,
|
||||
Qtype: qType,
|
||||
@@ -530,25 +525,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
}
|
||||
|
||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||
return nil
|
||||
}
|
||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||
for _, rawAnswer := range response.Answer {
|
||||
switch answer := rawAnswer.(type) {
|
||||
case *dns.A:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.A))
|
||||
case *dns.AAAA:
|
||||
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
|
||||
case *dns.HTTPS:
|
||||
for _, value := range answer.SVCB.Value {
|
||||
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
|
||||
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return addresses
|
||||
return adapter.DNSResponseAddresses(response)
|
||||
}
|
||||
|
||||
func wrapError(err error) error {
|
||||
|
||||
111
dns/repro_test.go
Normal file
111
dns/repro_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"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"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
||||
var qTypes []uint16
|
||||
router := newTestRouter(t, nil, &fakeDNSTransportManager{
|
||||
defaultTransport: defaultTransport,
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"default": defaultTransport,
|
||||
},
|
||||
}, &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
qTypes = append(qTypes, message.Question[0].Qtype)
|
||||
if message.Question[0].Qtype == mDNS.TypeA {
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
|
||||
}
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
|
||||
},
|
||||
})
|
||||
|
||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
||||
Strategy: C.DomainStrategyIPv4Only,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
|
||||
}
|
||||
|
||||
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
transportManager := &fakeDNSTransportManager{
|
||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
transports: map[string]adapter.DNSTransport{
|
||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
||||
},
|
||||
}
|
||||
client := &fakeDNSClient{
|
||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
switch transport.Tag() {
|
||||
case "upstream":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
|
||||
case "selected":
|
||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
||||
default:
|
||||
return nil, E.New("unexpected transport")
|
||||
}
|
||||
},
|
||||
}
|
||||
rules := []option.DNSRule{
|
||||
{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
Domain: badoption.Listable[string]{"example.com"},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeEvaluate,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: C.RuleTypeLogical,
|
||||
LogicalOptions: option.LogicalDNSRule{
|
||||
RawLogicalDNSRule: option.RawLogicalDNSRule{
|
||||
Mode: C.LogicalTypeOr,
|
||||
Rules: []option.DNSRule{{
|
||||
Type: C.RuleTypeDefault,
|
||||
DefaultOptions: option.DefaultDNSRule{
|
||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
||||
MatchResponse: true,
|
||||
IPCIDR: badoption.Listable[string]{"1.1.1.0/24"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
DNSRuleAction: option.DNSRuleAction{
|
||||
Action: C.RuleActionTypeRoute,
|
||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
router := newTestRouter(t, rules, transportManager, client)
|
||||
|
||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
||||
}, adapter.DNSQueryOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
||||
}
|
||||
788
dns/router.go
788
dns/router.go
@@ -5,11 +5,13 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
R "github.com/sagernet/sing-box/route/rule"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
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"
|
||||
@@ -26,7 +29,10 @@ import (
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var _ adapter.DNSRouter = (*Router)(nil)
|
||||
var (
|
||||
_ adapter.DNSRouter = (*Router)(nil)
|
||||
_ adapter.DNSRuleSetUpdateValidator = (*Router)(nil)
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
ctx context.Context
|
||||
@@ -34,10 +40,15 @@ type Router struct {
|
||||
transport adapter.DNSTransportManager
|
||||
outbound adapter.OutboundManager
|
||||
client adapter.DNSClient
|
||||
rawRules []option.DNSRule
|
||||
rules []adapter.DNSRule
|
||||
defaultDomainStrategy C.DomainStrategy
|
||||
dnsReverseMapping freelru.Cache[netip.Addr, string]
|
||||
platformInterface adapter.PlatformInterface
|
||||
legacyDNSMode bool
|
||||
rulesAccess sync.RWMutex
|
||||
started bool
|
||||
closing bool
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
|
||||
@@ -46,6 +57,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
logger: logFactory.NewLogger("dns"),
|
||||
transport: service.FromContext[adapter.DNSTransportManager](ctx),
|
||||
outbound: service.FromContext[adapter.OutboundManager](ctx),
|
||||
rawRules: make([]option.DNSRule, 0, len(options.Rules)),
|
||||
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
|
||||
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
|
||||
}
|
||||
@@ -74,13 +86,12 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
|
||||
}
|
||||
|
||||
func (r *Router) Initialize(rules []option.DNSRule) error {
|
||||
for i, ruleOptions := range rules {
|
||||
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
r.rules = append(r.rules, dnsRule)
|
||||
r.rawRules = append(r.rawRules[:0], rules...)
|
||||
newRules, _, _, err := r.buildRules(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
closeRules(newRules)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -92,32 +103,146 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
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, "]")
|
||||
}
|
||||
monitor.Start("initialize DNS rules")
|
||||
newRules, legacyDNSMode, modeFlags, err := r.buildRules(true)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.rulesAccess.Lock()
|
||||
if r.closing {
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(newRules)
|
||||
return nil
|
||||
}
|
||||
r.rules = newRules
|
||||
r.legacyDNSMode = legacyDNSMode
|
||||
r.started = true
|
||||
r.rulesAccess.Unlock()
|
||||
if legacyDNSMode && common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() }) {
|
||||
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter)
|
||||
}
|
||||
if legacyDNSMode && modeFlags.neededFromStrategy {
|
||||
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) Close() error {
|
||||
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()
|
||||
r.rulesAccess.Lock()
|
||||
if r.closing {
|
||||
r.rulesAccess.Unlock()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
r.closing = true
|
||||
runtimeRules := r.rules
|
||||
r.rules = nil
|
||||
r.rulesAccess.Unlock()
|
||||
closeRules(runtimeRules)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) {
|
||||
for i, ruleOptions := range r.rawRules {
|
||||
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
|
||||
if err != nil {
|
||||
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
}
|
||||
router := service.FromContext[adapter.Router](r.ctx)
|
||||
legacyDNSMode, modeFlags, err := resolveLegacyDNSMode(router, r.rawRules, nil)
|
||||
if err != nil {
|
||||
return nil, false, dnsRuleModeFlags{}, err
|
||||
}
|
||||
if !legacyDNSMode {
|
||||
err = validateLegacyDNSModeDisabledRules(r.rawRules)
|
||||
if err != nil {
|
||||
return nil, false, dnsRuleModeFlags{}, err
|
||||
}
|
||||
}
|
||||
err = validateEvaluateFakeIPRules(r.rawRules, r.transport)
|
||||
if err != nil {
|
||||
return nil, false, dnsRuleModeFlags{}, err
|
||||
}
|
||||
newRules := make([]adapter.DNSRule, 0, len(r.rawRules))
|
||||
for i, ruleOptions := range r.rawRules {
|
||||
var dnsRule adapter.DNSRule
|
||||
dnsRule, err = R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode)
|
||||
if err != nil {
|
||||
closeRules(newRules)
|
||||
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "parse dns rule[", i, "]")
|
||||
}
|
||||
newRules = append(newRules, dnsRule)
|
||||
}
|
||||
if startRules {
|
||||
for i, rule := range newRules {
|
||||
err = rule.Start()
|
||||
if err != nil {
|
||||
closeRules(newRules)
|
||||
return nil, false, dnsRuleModeFlags{}, E.Cause(err, "initialize DNS rule[", i, "]")
|
||||
}
|
||||
}
|
||||
}
|
||||
return newRules, legacyDNSMode, modeFlags, nil
|
||||
}
|
||||
|
||||
func closeRules(rules []adapter.DNSRule) {
|
||||
for _, rule := range rules {
|
||||
_ = rule.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ValidateRuleSetMetadataUpdate(tag string, metadata adapter.RuleSetMetadata) error {
|
||||
if len(r.rawRules) == 0 {
|
||||
return nil
|
||||
}
|
||||
router := service.FromContext[adapter.Router](r.ctx)
|
||||
if router == nil {
|
||||
return E.New("router service not found")
|
||||
}
|
||||
overrides := map[string]adapter.RuleSetMetadata{
|
||||
tag: metadata,
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
started := r.started
|
||||
legacyDNSMode := r.legacyDNSMode
|
||||
closing := r.closing
|
||||
r.rulesAccess.RUnlock()
|
||||
if closing {
|
||||
return nil
|
||||
}
|
||||
if !started {
|
||||
candidateLegacyDNSMode, _, err := resolveLegacyDNSMode(router, r.rawRules, overrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !candidateLegacyDNSMode {
|
||||
return validateLegacyDNSModeDisabledRules(r.rawRules)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
candidateLegacyDNSMode, flags, err := resolveLegacyDNSMode(router, r.rawRules, overrides)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if legacyDNSMode {
|
||||
if !candidateLegacyDNSMode && flags.disabled {
|
||||
err := validateLegacyDNSModeDisabledRules(r.rawRules)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if candidateLegacyDNSMode {
|
||||
return E.New(deprecated.OptionLegacyDNSAddressFilter.MessageWithLink())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context, rules []adapter.DNSRule, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
@@ -126,22 +251,18 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
if ruleIndex != -1 {
|
||||
currentRuleIndex = ruleIndex + 1
|
||||
}
|
||||
for ; currentRuleIndex < len(r.rules); currentRuleIndex++ {
|
||||
currentRule := r.rules[currentRuleIndex]
|
||||
for ; currentRuleIndex < len(rules); currentRuleIndex++ {
|
||||
currentRule := rules[currentRuleIndex]
|
||||
if currentRule.WithAddressLimit() && !isAddressQuery {
|
||||
continue
|
||||
}
|
||||
metadata.ResetRuleCache()
|
||||
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())
|
||||
metadata.DestinationAddressMatchFromResponse = false
|
||||
if currentRule.LegacyPreMatch(metadata) {
|
||||
if ruleDescription := currentRule.String(); ruleDescription != "" {
|
||||
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
|
||||
} else {
|
||||
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
|
||||
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *R.RuleActionDNSRoute:
|
||||
@@ -166,14 +287,6 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
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 {
|
||||
@@ -196,15 +309,270 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
|
||||
}
|
||||
}
|
||||
transport := r.transport.Default()
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
return transport, nil, -1
|
||||
}
|
||||
|
||||
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
|
||||
// Strategy is intentionally skipped here. A non-default DNS rule action strategy
|
||||
// forces legacy mode via resolveLegacyDNSMode, so this path is only reachable
|
||||
// when strategy remains at its default value.
|
||||
if routeOptions.DisableCache {
|
||||
options.DisableCache = true
|
||||
}
|
||||
if routeOptions.RewriteTTL != nil {
|
||||
options.RewriteTTL = routeOptions.RewriteTTL
|
||||
}
|
||||
if routeOptions.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = routeOptions.ClientSubnet
|
||||
}
|
||||
}
|
||||
|
||||
type dnsRouteStatus uint8
|
||||
|
||||
const (
|
||||
dnsRouteStatusMissing dnsRouteStatus = iota
|
||||
dnsRouteStatusSkipped
|
||||
dnsRouteStatusResolved
|
||||
)
|
||||
|
||||
func (r *Router) resolveDNSRoute(server string, routeOptions R.RuleActionDNSRouteOptions, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) {
|
||||
transport, loaded := r.transport.Transport(server)
|
||||
if !loaded {
|
||||
return nil, dnsRouteStatusMissing
|
||||
}
|
||||
isFakeIP := transport.Type() == C.DNSTypeFakeIP
|
||||
if isFakeIP && !allowFakeIP {
|
||||
return transport, dnsRouteStatusSkipped
|
||||
}
|
||||
r.applyDNSRouteOptions(options, routeOptions)
|
||||
if isFakeIP {
|
||||
options.DisableCache = true
|
||||
}
|
||||
return transport, dnsRouteStatusResolved
|
||||
}
|
||||
|
||||
func (r *Router) logRuleMatch(ctx context.Context, ruleIndex int, currentRule adapter.DNSRule) {
|
||||
if ruleDescription := currentRule.String(); ruleDescription != "" {
|
||||
r.logger.DebugContext(ctx, "match[", ruleIndex, "] ", currentRule, " => ", currentRule.Action())
|
||||
} else {
|
||||
r.logger.DebugContext(ctx, "match[", ruleIndex, "] => ", currentRule.Action())
|
||||
}
|
||||
}
|
||||
|
||||
type exchangeWithRulesResult struct {
|
||||
response *mDNS.Msg
|
||||
transport adapter.DNSTransport
|
||||
rejectAction *R.RuleActionReject
|
||||
err error
|
||||
}
|
||||
|
||||
const dnsRespondMissingResponseMessage = "respond action requires an evaluated response from a preceding evaluate action"
|
||||
|
||||
func (r *Router) exchangeWithRules(ctx context.Context, rules []adapter.DNSRule, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult {
|
||||
metadata := adapter.ContextFrom(ctx)
|
||||
if metadata == nil {
|
||||
panic("no context")
|
||||
}
|
||||
effectiveOptions := options
|
||||
var evaluatedResponse *mDNS.Msg
|
||||
var evaluatedTransport adapter.DNSTransport
|
||||
for currentRuleIndex, currentRule := range rules {
|
||||
metadata.ResetRuleCache()
|
||||
metadata.DNSResponse = evaluatedResponse
|
||||
metadata.DestinationAddressMatchFromResponse = false
|
||||
if !currentRule.Match(metadata) {
|
||||
continue
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
r.logRuleMatch(ctx, currentRuleIndex, currentRule)
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *R.RuleActionDNSRouteOptions:
|
||||
r.applyDNSRouteOptions(&effectiveOptions, *action)
|
||||
case *R.RuleActionEvaluate:
|
||||
queryOptions := effectiveOptions
|
||||
transport, loaded := r.transport.Transport(action.Server)
|
||||
if !loaded {
|
||||
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
||||
evaluatedResponse = nil
|
||||
evaluatedTransport = nil
|
||||
continue
|
||||
}
|
||||
r.applyDNSRouteOptions(&queryOptions, action.RuleActionDNSRouteOptions)
|
||||
exchangeOptions := queryOptions
|
||||
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
|
||||
exchangeOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
|
||||
if err != nil {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||
evaluatedResponse = nil
|
||||
evaluatedTransport = nil
|
||||
continue
|
||||
}
|
||||
evaluatedResponse = response
|
||||
evaluatedTransport = transport
|
||||
case *R.RuleActionRespond:
|
||||
if evaluatedResponse == nil {
|
||||
return exchangeWithRulesResult{
|
||||
err: E.New(dnsRespondMissingResponseMessage),
|
||||
}
|
||||
}
|
||||
return exchangeWithRulesResult{
|
||||
response: evaluatedResponse,
|
||||
transport: evaluatedTransport,
|
||||
}
|
||||
case *R.RuleActionDNSRoute:
|
||||
queryOptions := effectiveOptions
|
||||
transport, status := r.resolveDNSRoute(action.Server, action.RuleActionDNSRouteOptions, allowFakeIP, &queryOptions)
|
||||
switch status {
|
||||
case dnsRouteStatusMissing:
|
||||
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
|
||||
continue
|
||||
case dnsRouteStatusSkipped:
|
||||
continue
|
||||
}
|
||||
exchangeOptions := queryOptions
|
||||
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
|
||||
exchangeOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
|
||||
return exchangeWithRulesResult{
|
||||
response: response,
|
||||
transport: transport,
|
||||
err: err,
|
||||
}
|
||||
case *R.RuleActionReject:
|
||||
switch action.Method {
|
||||
case C.RuleActionRejectMethodDefault:
|
||||
return exchangeWithRulesResult{
|
||||
response: &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: message.Id,
|
||||
Rcode: mDNS.RcodeRefused,
|
||||
Response: true,
|
||||
},
|
||||
Question: []mDNS.Question{message.Question[0]},
|
||||
},
|
||||
rejectAction: action,
|
||||
}
|
||||
case C.RuleActionRejectMethodDrop:
|
||||
return exchangeWithRulesResult{
|
||||
rejectAction: action,
|
||||
err: tun.ErrDrop,
|
||||
}
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return exchangeWithRulesResult{
|
||||
response: action.Response(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
return transport, nil, -1
|
||||
transport := r.transport.Default()
|
||||
exchangeOptions := effectiveOptions
|
||||
if exchangeOptions.Strategy == C.DomainStrategyAsIS {
|
||||
exchangeOptions.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err := r.client.Exchange(adapter.OverrideContext(ctx), transport, message, exchangeOptions, nil)
|
||||
return exchangeWithRulesResult{
|
||||
response: response,
|
||||
transport: transport,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) resolveLookupStrategy(options adapter.DNSQueryOptions) C.DomainStrategy {
|
||||
if options.LookupStrategy != C.DomainStrategyAsIS {
|
||||
return options.LookupStrategy
|
||||
}
|
||||
if options.Strategy != C.DomainStrategyAsIS {
|
||||
return options.Strategy
|
||||
}
|
||||
return r.defaultDomainStrategy
|
||||
}
|
||||
|
||||
func withLookupQueryMetadata(ctx context.Context, qType uint16) context.Context {
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.QueryType = qType
|
||||
metadata.IPVersion = 0
|
||||
switch qType {
|
||||
case mDNS.TypeA:
|
||||
metadata.IPVersion = 4
|
||||
case mDNS.TypeAAAA:
|
||||
metadata.IPVersion = 6
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func filterAddressesByQueryType(addresses []netip.Addr, qType uint16) []netip.Addr {
|
||||
switch qType {
|
||||
case mDNS.TypeA:
|
||||
return common.Filter(addresses, func(address netip.Addr) bool {
|
||||
return address.Is4()
|
||||
})
|
||||
case mDNS.TypeAAAA:
|
||||
return common.Filter(addresses, func(address netip.Addr) bool {
|
||||
return address.Is6()
|
||||
})
|
||||
default:
|
||||
return addresses
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) lookupWithRules(ctx context.Context, rules []adapter.DNSRule, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
strategy := r.resolveLookupStrategy(options)
|
||||
lookupOptions := options
|
||||
if strategy != C.DomainStrategyAsIS {
|
||||
lookupOptions.Strategy = strategy
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv4Only {
|
||||
return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
|
||||
}
|
||||
if strategy == C.DomainStrategyIPv6Only {
|
||||
return r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
}
|
||||
var (
|
||||
response4 []netip.Addr
|
||||
response6 []netip.Addr
|
||||
)
|
||||
var group task.Group
|
||||
group.Append("exchange4", func(ctx context.Context) error {
|
||||
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeA, lookupOptions)
|
||||
response4 = result
|
||||
return err
|
||||
})
|
||||
group.Append("exchange6", func(ctx context.Context) error {
|
||||
result, err := r.lookupWithRulesType(ctx, rules, domain, mDNS.TypeAAAA, lookupOptions)
|
||||
response6 = result
|
||||
return err
|
||||
})
|
||||
err := group.Run(ctx)
|
||||
if len(response4) == 0 && len(response6) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return sortAddresses(response4, response6, strategy), nil
|
||||
}
|
||||
|
||||
func (r *Router) lookupWithRulesType(ctx context.Context, rules []adapter.DNSRule, domain string, qType uint16, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{{
|
||||
Name: mDNS.Fqdn(domain),
|
||||
Qtype: qType,
|
||||
Qclass: mDNS.ClassINET,
|
||||
}},
|
||||
}
|
||||
exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), rules, request, options, false)
|
||||
if exchangeResult.rejectAction != nil {
|
||||
return nil, exchangeResult.rejectAction.Error(ctx)
|
||||
}
|
||||
if exchangeResult.err != nil {
|
||||
return nil, exchangeResult.err
|
||||
}
|
||||
if exchangeResult.response.Rcode != mDNS.RcodeSuccess {
|
||||
return nil, RcodeError(exchangeResult.response.Rcode)
|
||||
}
|
||||
return filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType), nil
|
||||
}
|
||||
|
||||
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
|
||||
@@ -220,6 +588,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
}
|
||||
return &responseMessage, nil
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
if r.closing {
|
||||
return nil, E.New("dns router closed")
|
||||
}
|
||||
rules := r.rules
|
||||
legacyDNSMode := r.legacyDNSMode
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
var (
|
||||
response *mDNS.Msg
|
||||
@@ -230,6 +605,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
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
|
||||
@@ -239,18 +616,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||
if options.Transport != nil {
|
||||
transport = options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||
} else if !legacyDNSMode {
|
||||
exchangeResult := r.exchangeWithRules(ctx, rules, message, options, true)
|
||||
response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err
|
||||
} else {
|
||||
var (
|
||||
rule adapter.DNSRule
|
||||
@@ -260,7 +632,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, rules, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
@@ -278,7 +650,9 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
return nil, tun.ErrDrop
|
||||
}
|
||||
case *R.RuleActionPredefined:
|
||||
return action.Response(message), nil
|
||||
err = nil
|
||||
response = action.Response(message)
|
||||
goto done
|
||||
}
|
||||
}
|
||||
responseCheck := addressLimitResponseCheck(rule, metadata)
|
||||
@@ -306,6 +680,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
break
|
||||
}
|
||||
}
|
||||
done:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -325,6 +700,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
}
|
||||
|
||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
if r.closing {
|
||||
return nil, E.New("dns router closed")
|
||||
}
|
||||
rules := r.rules
|
||||
legacyDNSMode := r.legacyDNSMode
|
||||
var (
|
||||
responseAddrs []netip.Addr
|
||||
err error
|
||||
@@ -338,6 +720,8 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
|
||||
} else if errors.Is(err, ErrResponseRejected) {
|
||||
r.logger.DebugContext(ctx, "response rejected for ", domain)
|
||||
} else if R.IsRejected(err) {
|
||||
r.logger.DebugContext(ctx, "lookup rejected for ", domain)
|
||||
} else {
|
||||
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
|
||||
}
|
||||
@@ -350,20 +734,16 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
ctx, metadata := adapter.ExtendContext(ctx)
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
metadata.Domain = FqdnToDomain(domain)
|
||||
metadata.DNSResponse = nil
|
||||
metadata.DestinationAddressMatchFromResponse = false
|
||||
if options.Transport != nil {
|
||||
transport := options.Transport
|
||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = legacyTransport.LegacyStrategy()
|
||||
}
|
||||
if !options.ClientSubnet.IsValid() {
|
||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||
}
|
||||
}
|
||||
if options.Strategy == C.DomainStrategyAsIS {
|
||||
options.Strategy = r.defaultDomainStrategy
|
||||
}
|
||||
responseAddrs, err = r.client.Lookup(ctx, transport, domain, options, nil)
|
||||
} else if !legacyDNSMode {
|
||||
responseAddrs, err = r.lookupWithRules(ctx, rules, domain, options)
|
||||
} else {
|
||||
var (
|
||||
transport adapter.DNSTransport
|
||||
@@ -374,7 +754,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
||||
for {
|
||||
dnsCtx := adapter.OverrideContext(ctx)
|
||||
dnsOptions := options
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true, &dnsOptions)
|
||||
transport, rule, ruleIndex = r.matchDNS(ctx, rules, false, ruleIndex, true, &dnsOptions)
|
||||
if rule != nil {
|
||||
switch action := rule.Action().(type) {
|
||||
case *R.RuleActionReject:
|
||||
@@ -425,15 +805,14 @@ func isAddressQuery(message *mDNS.Msg) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
|
||||
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool {
|
||||
if rule == nil || !rule.WithAddressLimit() {
|
||||
return nil
|
||||
}
|
||||
responseMetadata := *metadata
|
||||
return func(responseAddrs []netip.Addr) bool {
|
||||
return func(response *mDNS.Msg) bool {
|
||||
checkMetadata := responseMetadata
|
||||
checkMetadata.DestinationAddresses = responseAddrs
|
||||
return rule.MatchAddressLimit(&checkMetadata)
|
||||
return rule.MatchAddressLimit(&checkMetadata, response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,3 +837,268 @@ func (r *Router) ResetNetwork() {
|
||||
transport.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool {
|
||||
if rule.IPAcceptAny || rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
|
||||
return true
|
||||
}
|
||||
return !rule.MatchResponse && (len(rule.IPCIDR) > 0 || rule.IPIsPrivate)
|
||||
}
|
||||
|
||||
func hasResponseMatchFields(rule option.DefaultDNSRule) bool {
|
||||
return rule.ResponseRcode != nil ||
|
||||
len(rule.ResponseAnswer) > 0 ||
|
||||
len(rule.ResponseNs) > 0 ||
|
||||
len(rule.ResponseExtra) > 0
|
||||
}
|
||||
|
||||
func defaultRuleDisablesLegacyDNSMode(rule option.DefaultDNSRule) bool {
|
||||
return rule.MatchResponse ||
|
||||
hasResponseMatchFields(rule) ||
|
||||
rule.Action == C.RuleActionTypeEvaluate ||
|
||||
rule.Action == C.RuleActionTypeRespond ||
|
||||
rule.IPVersion > 0 ||
|
||||
len(rule.QueryType) > 0
|
||||
}
|
||||
|
||||
type dnsRuleModeFlags struct {
|
||||
disabled bool
|
||||
needed bool
|
||||
neededFromStrategy bool
|
||||
}
|
||||
|
||||
func (f *dnsRuleModeFlags) merge(other dnsRuleModeFlags) {
|
||||
f.disabled = f.disabled || other.disabled
|
||||
f.needed = f.needed || other.needed
|
||||
f.neededFromStrategy = f.neededFromStrategy || other.neededFromStrategy
|
||||
}
|
||||
|
||||
func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (bool, dnsRuleModeFlags, error) {
|
||||
flags, err := dnsRuleModeRequirements(router, rules, metadataOverrides)
|
||||
if err != nil {
|
||||
return false, flags, err
|
||||
}
|
||||
if flags.disabled && flags.neededFromStrategy {
|
||||
return false, flags, E.New(deprecated.OptionLegacyDNSRuleStrategy.MessageWithLink())
|
||||
}
|
||||
if flags.disabled {
|
||||
return false, flags, nil
|
||||
}
|
||||
return flags.needed, flags, nil
|
||||
}
|
||||
|
||||
func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
|
||||
var flags dnsRuleModeFlags
|
||||
for i, rule := range rules {
|
||||
ruleFlags, err := dnsRuleModeRequirementsInRule(router, rule, metadataOverrides)
|
||||
if err != nil {
|
||||
return dnsRuleModeFlags{}, E.Cause(err, "dns rule[", i, "]")
|
||||
}
|
||||
flags.merge(ruleFlags)
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
|
||||
case C.RuleTypeLogical:
|
||||
flags := dnsRuleModeFlags{
|
||||
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond,
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
|
||||
}
|
||||
flags.needed = flags.neededFromStrategy
|
||||
for i, subRule := range rule.LogicalOptions.Rules {
|
||||
subFlags, err := dnsRuleModeRequirementsInRule(router, subRule, metadataOverrides)
|
||||
if err != nil {
|
||||
return dnsRuleModeFlags{}, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
flags.merge(subFlags)
|
||||
}
|
||||
return flags, nil
|
||||
default:
|
||||
return dnsRuleModeFlags{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
|
||||
flags := dnsRuleModeFlags{
|
||||
disabled: defaultRuleDisablesLegacyDNSMode(rule),
|
||||
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
|
||||
}
|
||||
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
|
||||
if len(rule.RuleSet) == 0 {
|
||||
return flags, nil
|
||||
}
|
||||
if router == nil {
|
||||
return dnsRuleModeFlags{}, E.New("router service not found")
|
||||
}
|
||||
for _, tag := range rule.RuleSet {
|
||||
metadata, err := lookupDNSRuleSetMetadata(router, tag, metadataOverrides)
|
||||
if err != nil {
|
||||
return dnsRuleModeFlags{}, err
|
||||
}
|
||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
||||
flags.disabled = flags.disabled || metadata.ContainsDNSQueryTypeRule
|
||||
if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule {
|
||||
flags.needed = true
|
||||
}
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func lookupDNSRuleSetMetadata(router adapter.Router, tag string, metadataOverrides map[string]adapter.RuleSetMetadata) (adapter.RuleSetMetadata, error) {
|
||||
if metadataOverrides != nil {
|
||||
if metadata, loaded := metadataOverrides[tag]; loaded {
|
||||
return metadata, nil
|
||||
}
|
||||
}
|
||||
ruleSet, loaded := router.RuleSet(tag)
|
||||
if !loaded {
|
||||
return adapter.RuleSetMetadata{}, E.New("rule-set not found: ", tag)
|
||||
}
|
||||
return ruleSet.Metadata(), nil
|
||||
}
|
||||
|
||||
func referencedDNSRuleSetTags(rules []option.DNSRule) []string {
|
||||
tagMap := make(map[string]bool)
|
||||
var walkRule func(rule option.DNSRule)
|
||||
walkRule = func(rule option.DNSRule) {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
for _, tag := range rule.DefaultOptions.RuleSet {
|
||||
tagMap[tag] = true
|
||||
}
|
||||
case C.RuleTypeLogical:
|
||||
for _, subRule := range rule.LogicalOptions.Rules {
|
||||
walkRule(subRule)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, rule := range rules {
|
||||
walkRule(rule)
|
||||
}
|
||||
tags := make([]string, 0, len(tagMap))
|
||||
for tag := range tagMap {
|
||||
if tag != "" {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
func validateLegacyDNSModeDisabledRules(rules []option.DNSRule) error {
|
||||
var seenEvaluate bool
|
||||
for i, rule := range rules {
|
||||
requiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(rule)
|
||||
if err != nil {
|
||||
return E.Cause(err, "validate dns rule[", i, "]")
|
||||
}
|
||||
if requiresPriorEvaluate && !seenEvaluate {
|
||||
return E.New("dns rule[", i, "]: response-based matching requires a preceding evaluate action")
|
||||
}
|
||||
if dnsRuleActionType(rule) == C.RuleActionTypeEvaluate {
|
||||
seenEvaluate = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEvaluateFakeIPRules(rules []option.DNSRule, transportManager adapter.DNSTransportManager) error {
|
||||
if transportManager == nil {
|
||||
return nil
|
||||
}
|
||||
for i, rule := range rules {
|
||||
if dnsRuleActionType(rule) != C.RuleActionTypeEvaluate {
|
||||
continue
|
||||
}
|
||||
server := dnsRuleActionServer(rule)
|
||||
if server == "" {
|
||||
continue
|
||||
}
|
||||
transport, loaded := transportManager.Transport(server)
|
||||
if !loaded || transport.Type() != C.DNSTypeFakeIP {
|
||||
continue
|
||||
}
|
||||
return E.New("dns rule[", i, "]: evaluate action cannot use fakeip server: ", server)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions)
|
||||
case C.RuleTypeLogical:
|
||||
requiresPriorEvaluate := dnsRuleActionType(rule) == C.RuleActionTypeRespond
|
||||
for i, subRule := range rule.LogicalOptions.Rules {
|
||||
subRequiresPriorEvaluate, err := validateLegacyDNSModeDisabledRuleTree(subRule)
|
||||
if err != nil {
|
||||
return false, E.Cause(err, "sub rule[", i, "]")
|
||||
}
|
||||
requiresPriorEvaluate = requiresPriorEvaluate || subRequiresPriorEvaluate
|
||||
}
|
||||
return requiresPriorEvaluate, nil
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) {
|
||||
hasResponseRecords := hasResponseMatchFields(rule)
|
||||
if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse {
|
||||
return false, E.New("Response Match Fields (ip_cidr, ip_is_private, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled")
|
||||
}
|
||||
// Intentionally do not reject rule_set here. A referenced rule set may mix
|
||||
// destination-IP predicates with pre-response predicates such as domain items.
|
||||
// When match_response is false, those destination-IP branches fail closed during
|
||||
// pre-response evaluation instead of consuming DNS response state, while sibling
|
||||
// non-response branches remain matchable.
|
||||
if rule.IPAcceptAny { //nolint:staticcheck
|
||||
return false, E.New(deprecated.OptionIPAcceptAny.MessageWithLink())
|
||||
}
|
||||
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
|
||||
return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink())
|
||||
}
|
||||
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil
|
||||
}
|
||||
|
||||
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
|
||||
switch action.Action {
|
||||
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
|
||||
return C.DomainStrategy(action.RouteOptions.Strategy) != C.DomainStrategyAsIS
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
return C.DomainStrategy(action.RouteOptionsOptions.Strategy) != C.DomainStrategyAsIS
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRuleActionType(rule option.DNSRule) string {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
if rule.DefaultOptions.Action == "" {
|
||||
return C.RuleActionTypeRoute
|
||||
}
|
||||
return rule.DefaultOptions.Action
|
||||
case C.RuleTypeLogical:
|
||||
if rule.LogicalOptions.Action == "" {
|
||||
return C.RuleActionTypeRoute
|
||||
}
|
||||
return rule.LogicalOptions.Action
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRuleActionServer(rule option.DNSRule) string {
|
||||
switch rule.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
return rule.DefaultOptions.RouteOptions.Server
|
||||
case C.RuleTypeLogical:
|
||||
return rule.LogicalOptions.RouteOptions.Server
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
2547
dns/router_test.go
Normal file
2547
dns/router_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,13 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
||||
|
||||
type TransportAdapter struct {
|
||||
transportType string
|
||||
transportTag string
|
||||
dependencies []string
|
||||
strategy C.DomainStrategy
|
||||
clientSubnet netip.Prefix
|
||||
}
|
||||
|
||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
||||
@@ -35,8 +27,6 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
||||
clientSubnet: localOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +35,10 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
|
||||
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
||||
}
|
||||
if remoteOptions.LegacyAddressResolver != "" {
|
||||
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
||||
}
|
||||
return TransportAdapter{
|
||||
transportType: transportType,
|
||||
transportTag: transportTag,
|
||||
dependencies: dependencies,
|
||||
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
||||
clientSubnet: remoteOptions.LegacyClientSubnet,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,11 +53,3 @@ func (a *TransportAdapter) Tag() string {
|
||||
func (a *TransportAdapter) Dependencies() []string {
|
||||
return a.dependencies
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
||||
return a.strategy
|
||||
}
|
||||
|
||||
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
||||
return a.clientSubnet
|
||||
}
|
||||
|
||||
@@ -2,104 +2,25 @@ package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
||||
if options.LegacyDefaultDialer {
|
||||
return dialer.NewDefaultOutbound(ctx), nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
DirectResolver: true,
|
||||
})
|
||||
}
|
||||
|
||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
||||
if options.LegacyDefaultDialer {
|
||||
transportDialer := dialer.NewDefaultOutbound(ctx)
|
||||
if options.LegacyAddressResolver != "" {
|
||||
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
||||
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
|
||||
if !loaded {
|
||||
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
|
||||
}
|
||||
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
|
||||
} else if options.ServerIsDomain() {
|
||||
return nil, E.New("missing address resolver for server: ", options.Server)
|
||||
}
|
||||
return transportDialer, nil
|
||||
} else {
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
LegacyDNSDialer: options.Legacy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type legacyTransportDialer struct {
|
||||
dialer N.Dialer
|
||||
dnsRouter adapter.DNSRouter
|
||||
transport adapter.DNSTransport
|
||||
strategy C.DomainStrategy
|
||||
fallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
|
||||
return &legacyTransportDialer{
|
||||
dialer,
|
||||
dnsRouter,
|
||||
transport,
|
||||
strategy,
|
||||
fallbackDelay,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
return dialer.NewWithOptions(dialer.Options{
|
||||
Context: ctx,
|
||||
Options: options.DialerOptions,
|
||||
RemoteIsDomain: options.ServerIsDomain(),
|
||||
DirectResolver: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if destination.IsIP() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||
Transport: d.transport,
|
||||
Strategy: d.strategy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
func (d *legacyTransportDialer) Upstream() any {
|
||||
return d.dialer
|
||||
}
|
||||
|
||||
@@ -663,7 +663,7 @@ DNS servers are refactored for better performance and scalability.
|
||||
|
||||
See [DNS server](/configuration/dns/server/).
|
||||
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
||||
|
||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||
|
||||
@@ -1133,7 +1133,7 @@ DNS servers are refactored for better performance and scalability.
|
||||
|
||||
See [DNS server](/configuration/dns/server/).
|
||||
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
||||
|
||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||
|
||||
@@ -1969,7 +1969,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||
if using this method.
|
||||
|
||||
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
||||
|
||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||
|
||||
@@ -1983,7 +1983,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
|
||||
**5**:
|
||||
|
||||
The new feature allows you to cache the check results of
|
||||
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
||||
|
||||
**6**:
|
||||
|
||||
@@ -2164,7 +2164,7 @@ See [TUN](/configuration/inbound/tun) inbound.
|
||||
**1**:
|
||||
|
||||
The new feature allows you to cache the check results of
|
||||
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
||||
|
||||
#### 1.9.0-alpha.7
|
||||
|
||||
@@ -2211,7 +2211,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||
if using this method.
|
||||
|
||||
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
||||
|
||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/note-remove
|
||||
---
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
!!! failure "Removed in sing-box 1.14.0"
|
||||
|
||||
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||
Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -26,6 +26,6 @@ Enable FakeIP service.
|
||||
|
||||
IPv4 address range for FakeIP.
|
||||
|
||||
#### inet6_address
|
||||
#### inet6_range
|
||||
|
||||
IPv6 address range for FakeIP.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/note-remove
|
||||
---
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
!!! failure "已在 sing-box 1.14.0 移除"
|
||||
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ icon: material/alert-decagram
|
||||
|----------|---------------------------------|
|
||||
| `server` | List of [DNS Server](./server/) |
|
||||
| `rules` | List of [DNS Rule](./rule/) |
|
||||
| `fakeip` | [FakeIP](./fakeip/) |
|
||||
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
|
||||
|
||||
#### final
|
||||
|
||||
@@ -88,4 +88,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||
Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||
|
||||
@@ -88,6 +88,6 @@ LRU 缓存容量。
|
||||
|
||||
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
||||
|
||||
#### fakeip
|
||||
#### fakeip :material-note-remove:
|
||||
|
||||
[FakeIP](./fakeip/) 设置。
|
||||
|
||||
@@ -5,7 +5,14 @@ icon: material/alert-decagram
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-delete-clock: [ip_accept_any](#ip_accept_any)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
@@ -94,12 +101,6 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -171,7 +172,16 @@ icon: material/alert-decagram
|
||||
"geosite-cn"
|
||||
],
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"match_response": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
"response_extra": [],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@@ -180,7 +190,9 @@ icon: material/alert-decagram
|
||||
"server": "local",
|
||||
|
||||
// Deprecated
|
||||
|
||||
|
||||
"ip_accept_any": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
@@ -477,6 +489,19 @@ Make `ip_cidr` rule items in rule-sets match the source IP.
|
||||
|
||||
Make `ip_cidr` rule items in rule-sets match the source IP.
|
||||
|
||||
#### match_response
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Enable response-based matching. When enabled, this rule matches against the evaluated response
|
||||
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
|
||||
instead of only matching the original query.
|
||||
|
||||
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
|
||||
Also required for `ip_cidr` and `ip_is_private` when used with `evaluate` or Response Match Fields.
|
||||
|
||||
#### invert
|
||||
|
||||
Invert match result.
|
||||
@@ -521,7 +546,12 @@ See [DNS Rule Actions](../rule_action/) for details.
|
||||
|
||||
Moved to [DNS Rule Action](../rule_action#route).
|
||||
|
||||
### Address Filter Fields
|
||||
### Legacy Address Filter Fields
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
|
||||
|
||||
@@ -547,24 +577,73 @@ Match GeoIP with query response.
|
||||
|
||||
Match IP CIDR with query response.
|
||||
|
||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
#### ip_is_private
|
||||
|
||||
!!! question "Since sing-box 1.9.0"
|
||||
|
||||
Match private IP with query response.
|
||||
|
||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Make `ip_cidr` rules in rule-sets accept empty query response.
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`ip_accept_any` is deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Match any IP with query response.
|
||||
|
||||
### Response Match Fields
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Match fields for the evaluated response. Require `match_response` to be set to `true`
|
||||
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
|
||||
|
||||
That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
#### response_rcode
|
||||
|
||||
Match DNS response code.
|
||||
|
||||
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
|
||||
|
||||
#### response_answer
|
||||
|
||||
Match DNS answer records.
|
||||
|
||||
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
|
||||
|
||||
#### response_ns
|
||||
|
||||
Match DNS name server records.
|
||||
|
||||
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
|
||||
|
||||
#### response_extra
|
||||
|
||||
Match DNS extra records.
|
||||
|
||||
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
|
||||
|
||||
### Logical Fields
|
||||
|
||||
#### type
|
||||
|
||||
@@ -5,7 +5,14 @@ icon: material/alert-decagram
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-delete-clock: [ip_accept_any](#ip_accept_any)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
@@ -94,12 +101,6 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"source_ip_is_private": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"source_port": [
|
||||
12345
|
||||
],
|
||||
@@ -171,7 +172,16 @@ icon: material/alert-decagram
|
||||
"geosite-cn"
|
||||
],
|
||||
"rule_set_ip_cidr_match_source": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"match_response": false,
|
||||
"ip_cidr": [
|
||||
"10.0.0.0/24",
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
"response_extra": [],
|
||||
"invert": false,
|
||||
"outbound": [
|
||||
"direct"
|
||||
@@ -180,6 +190,9 @@ icon: material/alert-decagram
|
||||
"server": "local",
|
||||
|
||||
// 已弃用
|
||||
|
||||
"ip_accept_any": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
"cn"
|
||||
@@ -476,6 +489,17 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
使规则集中的 `ip_cidr` 规则匹配源 IP。
|
||||
|
||||
#### match_response
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
|
||||
|
||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
||||
|
||||
响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。
|
||||
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr` 和 `ip_is_private` 也需要此选项。
|
||||
|
||||
#### invert
|
||||
|
||||
反选匹配结果。
|
||||
@@ -520,7 +544,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
已移动到 [DNS 规则动作](../rule_action#route).
|
||||
|
||||
### 地址筛选字段
|
||||
### 旧版地址筛选字段
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||
|
||||
@@ -547,24 +576,73 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
与查询响应匹配 IP CIDR。
|
||||
|
||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
#### ip_is_private
|
||||
|
||||
!!! question "自 sing-box 1.9.0 起"
|
||||
|
||||
与查询响应匹配非公开 IP。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
匹配任意 IP。
|
||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
#### rule_set_ip_cidr_accept_empty
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
### 响应匹配字段
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
已评估的响应的匹配字段。需要将 `match_response` 设为 `true`,
|
||||
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
|
||||
|
||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
||||
|
||||
#### response_rcode
|
||||
|
||||
匹配 DNS 响应码。
|
||||
|
||||
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
|
||||
|
||||
#### response_answer
|
||||
|
||||
匹配 DNS 应答记录。
|
||||
|
||||
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
|
||||
|
||||
#### response_ns
|
||||
|
||||
匹配 DNS 名称服务器记录。
|
||||
|
||||
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
|
||||
|
||||
#### response_extra
|
||||
|
||||
匹配 DNS 额外记录。
|
||||
|
||||
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
|
||||
|
||||
### 逻辑字段
|
||||
|
||||
#### type
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
@@ -34,7 +40,11 @@ Tag of target server.
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Set domain strategy for this query.
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
|
||||
|
||||
Set domain strategy for this query. Deprecated, check [Migration](/migration/#migrate-dns-rule-action-strategy-to-rule-items).
|
||||
|
||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
@@ -52,7 +62,68 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will overrides `dns.client_subnet`.
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
### evaluate
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules
|
||||
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
|
||||
Unlike `route`, it does **not** terminate rule evaluation.
|
||||
|
||||
Only allowed on top-level DNS rules (not inside logical sub-rules).
|
||||
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
|
||||
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
|
||||
does not satisfy this requirement, because matching happens before the action runs.
|
||||
|
||||
#### server
|
||||
|
||||
==Required==
|
||||
|
||||
Tag of target server.
|
||||
|
||||
#### disable_cache
|
||||
|
||||
Disable cache and save cache in this query.
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
Rewrite TTL in DNS responses.
|
||||
|
||||
#### client_subnet
|
||||
|
||||
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
### respond
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "respond"
|
||||
}
|
||||
```
|
||||
|
||||
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
|
||||
|
||||
This action does not send a new DNS query and has no extra options.
|
||||
|
||||
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
|
||||
|
||||
### route-options
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-delete-clock: [strategy](#strategy)
|
||||
:material-plus: [evaluate](#evaluate)
|
||||
:material-plus: [respond](#respond)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [strategy](#strategy)
|
||||
@@ -34,7 +40,11 @@ icon: material/new-box
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
为此查询设置域名策略。
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
为此查询设置域名策略。已废弃,参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。
|
||||
|
||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
|
||||
@@ -54,6 +64,65 @@ icon: material/new-box
|
||||
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
### evaluate
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "",
|
||||
"disable_cache": false,
|
||||
"rewrite_ttl": null,
|
||||
"client_subnet": null
|
||||
}
|
||||
```
|
||||
|
||||
`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
|
||||
|
||||
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
|
||||
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
|
||||
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
|
||||
因为匹配发生在动作执行之前。
|
||||
|
||||
#### server
|
||||
|
||||
==必填==
|
||||
|
||||
目标 DNS 服务器的标签。
|
||||
|
||||
#### disable_cache
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
|
||||
#### client_subnet
|
||||
|
||||
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
||||
|
||||
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
||||
|
||||
将覆盖 `dns.client_subnet`.
|
||||
|
||||
### respond
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "respond"
|
||||
}
|
||||
```
|
||||
|
||||
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
|
||||
|
||||
此动作不会发起新的 DNS 查询,也没有额外选项。
|
||||
|
||||
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
|
||||
|
||||
### route-options
|
||||
|
||||
```json
|
||||
@@ -84,7 +153,7 @@ icon: material/new-box
|
||||
- `default`: 返回 REFUSED。
|
||||
- `drop`: 丢弃请求。
|
||||
|
||||
默认使用 `defualt`。
|
||||
默认使用 `default`。
|
||||
|
||||
#### no_drop
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ The type of the DNS server.
|
||||
|
||||
| Type | Format |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
|
||||
@@ -29,7 +29,7 @@ DNS 服务器的类型。
|
||||
|
||||
| 类型 | 格式 |
|
||||
|-----------------|---------------------------|
|
||||
| empty (default) | [Legacy](./legacy/) |
|
||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
||||
| `local` | [Local](./local/) |
|
||||
| `hosts` | [Hosts](./hosts/) |
|
||||
| `tcp` | [TCP](./tcp/) |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/note-remove
|
||||
---
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
!!! failure "Removed in sing-box 1.14.0"
|
||||
|
||||
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||
Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
||||
|
||||
!!! quote "Changes in sing-box 1.9.0"
|
||||
|
||||
@@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Can be overrides by `rules.[].client_subnet`.
|
||||
Can be overridden by `rules.[].client_subnet`.
|
||||
|
||||
Will overrides `dns.client_subnet`.
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
icon: material/delete-clock
|
||||
icon: material/note-remove
|
||||
---
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
!!! failure "已在 sing-box 1.14.0 移除"
|
||||
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Store fakeip in the cache file
|
||||
|
||||
Store rejected DNS response cache in the cache file
|
||||
|
||||
The check results of [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
|
||||
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
|
||||
will be cached until expiration.
|
||||
|
||||
#### rdrc_timeout
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
|
||||
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
|
||||
|
||||
#### rdrc_timeout
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea
|
||||
|
||||
See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details.
|
||||
|
||||
Can be overrides by `outbound.domain_resolver`.
|
||||
Can be overridden by `outbound.domain_resolver`.
|
||||
|
||||
#### default_network_strategy
|
||||
|
||||
@@ -163,7 +163,7 @@ See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
|
||||
|
||||
Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set.
|
||||
|
||||
Can be overrides by `outbound.network_strategy`.
|
||||
Can be overridden by `outbound.network_strategy`.
|
||||
|
||||
Conflicts with `default_interface`.
|
||||
|
||||
|
||||
@@ -316,4 +316,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
||||
|
||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||
|
||||
Will overrides `dns.client_subnet`.
|
||||
Will override `dns.client_subnet`.
|
||||
|
||||
@@ -14,14 +14,43 @@ check [Migration](../migration/#migrate-inline-acme-to-certificate-provider).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy `strategy` DNS rule action option
|
||||
|
||||
Legacy `strategy` DNS rule action option is deprecated,
|
||||
check [Migration](../migration/#migrate-dns-rule-action-strategy-to-rule-items).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy `ip_accept_any` DNS rule item
|
||||
|
||||
Legacy `ip_accept_any` DNS rule item is deprecated,
|
||||
check [Migration](../migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy `rule_set_ip_cidr_accept_empty` DNS rule item
|
||||
|
||||
Legacy `rule_set_ip_cidr_accept_empty` DNS rule item is deprecated,
|
||||
check [Migration](../migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Old fields will be removed in sing-box 1.16.0.
|
||||
|
||||
#### Legacy Address Filter Fields in DNS rules
|
||||
|
||||
Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`)
|
||||
in DNS rules are deprecated,
|
||||
check [Migration](../migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Old behavior will be removed in sing-box 1.16.0.
|
||||
|
||||
## 1.12.0
|
||||
|
||||
#### Legacy DNS server formats
|
||||
|
||||
DNS servers are refactored,
|
||||
check [Migration](../migration/#migrate-to-new-dns-servers).
|
||||
check [Migration](../migration/#migrate-to-new-dns-server-formats).
|
||||
|
||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||
Old formats were removed in sing-box 1.14.0.
|
||||
|
||||
#### `outbound` DNS rule item
|
||||
|
||||
|
||||
@@ -14,6 +14,34 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 旧版 DNS 规则动作 `strategy` 选项
|
||||
|
||||
旧版 DNS 规则动作 `strategy` 选项已废弃,
|
||||
参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 旧版 `ip_accept_any` DNS 规则项
|
||||
|
||||
旧版 `ip_accept_any` DNS 规则项已废弃,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项
|
||||
|
||||
旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项已废弃,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
旧字段将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
#### 旧版地址筛选字段 (DNS 规则)
|
||||
|
||||
旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
旧行为将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
## 1.12.0
|
||||
|
||||
#### 旧的 DNS 服务器格式
|
||||
@@ -21,7 +49,7 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
|
||||
DNS 服务器已重构,
|
||||
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
|
||||
|
||||
对旧格式的兼容性将在 sing-box 1.14.0 中被移除。
|
||||
旧格式已在 sing-box 1.14.0 中被移除。
|
||||
|
||||
#### `outbound` DNS 规则项
|
||||
|
||||
|
||||
@@ -79,6 +79,111 @@ See [ACME](/configuration/shared/certificate-provider/acme/) for fields newly ad
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate DNS rule action strategy to rule items
|
||||
|
||||
Legacy `strategy` DNS rule action option is deprecated.
|
||||
|
||||
In sing-box 1.14.0, internal domain resolution (Lookup) now splits A and AAAA queries
|
||||
at the rule level, so each query type is evaluated independently through the full rule chain.
|
||||
Use `ip_version` or `query_type` rule items to control which query types a rule matches.
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[DNS Rule](/configuration/dns/rule/) /
|
||||
[DNS Rule Action](/configuration/dns/rule_action/)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"domain_suffix": ".cn",
|
||||
"action": "route",
|
||||
"server": "local",
|
||||
"strategy": "ipv4_only"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"domain_suffix": ".cn",
|
||||
"ip_version": 4,
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Migrate address filter fields to response matching
|
||||
|
||||
Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`) in DNS rules are deprecated,
|
||||
along with Legacy `ip_accept_any` and Legacy `rule_set_ip_cidr_accept_empty` DNS rule items.
|
||||
|
||||
In sing-box 1.14.0, use the [`evaluate`](/configuration/dns/rule_action/#evaluate) action
|
||||
to fetch a DNS response, then match against it explicitly with `match_response`.
|
||||
|
||||
!!! info "References"
|
||||
|
||||
[DNS Rule](/configuration/dns/rule/) /
|
||||
[DNS Rule Action](/configuration/dns/rule_action/#evaluate)
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"rule_set": "geoip-cn",
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"action": "route",
|
||||
"server": "remote"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: New"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "remote"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"rule_set": "geoip-cn",
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"action": "route",
|
||||
"server": "remote"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1.12.0
|
||||
|
||||
### Migrate to new DNS server formats
|
||||
|
||||
@@ -79,6 +79,111 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移 DNS 规则动作 strategy 到规则项
|
||||
|
||||
旧版 DNS 规则动作 `strategy` 选项已废弃。
|
||||
|
||||
在 sing-box 1.14.0 中,内部域名解析(Lookup)现在在规则层拆分 A 和 AAAA 查询,
|
||||
每种查询类型独立通过完整的规则链评估。
|
||||
请使用 `ip_version` 或 `query_type` 规则项来控制规则匹配的查询类型。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[DNS 规则](/zh/configuration/dns/rule/) /
|
||||
[DNS 规则动作](/zh/configuration/dns/rule_action/)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"domain_suffix": ".cn",
|
||||
"action": "route",
|
||||
"server": "local",
|
||||
"strategy": "ipv4_only"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"domain_suffix": ".cn",
|
||||
"ip_version": 4,
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移地址筛选字段到响应匹配
|
||||
|
||||
旧版地址筛选字段(不使用 `match_response` 的 `ip_cidr`、`ip_is_private`)已废弃,
|
||||
旧版 `ip_accept_any` 和旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项也已废弃。
|
||||
|
||||
在 sing-box 1.14.0 中,请使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作
|
||||
获取 DNS 响应,然后通过 `match_response` 显式匹配。
|
||||
|
||||
!!! info "参考"
|
||||
|
||||
[DNS 规则](/zh/configuration/dns/rule/) /
|
||||
[DNS 规则动作](/zh/configuration/dns/rule_action/#evaluate)
|
||||
|
||||
=== ":material-card-remove: 弃用的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"rule_set": "geoip-cn",
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"action": "route",
|
||||
"server": "remote"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 新的"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "remote"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"rule_set": "geoip-cn",
|
||||
"action": "route",
|
||||
"server": "local"
|
||||
},
|
||||
{
|
||||
"action": "route",
|
||||
"server": "remote"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 1.12.0
|
||||
|
||||
### 迁移到新的 DNS 服务器格式
|
||||
|
||||
@@ -57,24 +57,6 @@ func (n Note) MessageWithLink() string {
|
||||
}
|
||||
}
|
||||
|
||||
var OptionLegacyDNSTransport = Note{
|
||||
Name: "legacy-dns-transport",
|
||||
Description: "legacy DNS servers",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "LEGACY_DNS_SERVERS",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats",
|
||||
}
|
||||
|
||||
var OptionLegacyDNSFakeIPOptions = Note{
|
||||
Name: "legacy-dns-fakeip-options",
|
||||
Description: "legacy DNS fakeip options",
|
||||
DeprecatedVersion: "1.12.0",
|
||||
ScheduledVersion: "1.14.0",
|
||||
EnvName: "LEGACY_DNS_FAKEIP_OPTIONS",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats",
|
||||
}
|
||||
|
||||
var OptionOutboundDNSRuleItem = Note{
|
||||
Name: "outbound-dns-rule-item",
|
||||
Description: "outbound DNS rule item",
|
||||
@@ -111,11 +93,49 @@ var OptionInlineACME = Note{
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-inline-acme-to-certificate-provider",
|
||||
}
|
||||
|
||||
var OptionIPAcceptAny = Note{
|
||||
Name: "dns-rule-ip-accept-any",
|
||||
Description: "Legacy `ip_accept_any` DNS rule item",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "DNS_RULE_IP_ACCEPT_ANY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching",
|
||||
}
|
||||
|
||||
var OptionRuleSetIPCIDRAcceptEmpty = Note{
|
||||
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
|
||||
Description: "Legacy `rule_set_ip_cidr_accept_empty` DNS rule item",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "DNS_RULE_RULE_SET_IP_CIDR_ACCEPT_EMPTY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching",
|
||||
}
|
||||
|
||||
var OptionLegacyDNSAddressFilter = Note{
|
||||
Name: "legacy-dns-address-filter",
|
||||
Description: "Legacy Address Filter Fields in DNS rules",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_DNS_ADDRESS_FILTER",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-address-filter-fields-to-response-matching",
|
||||
}
|
||||
|
||||
var OptionLegacyDNSRuleStrategy = Note{
|
||||
Name: "legacy-dns-rule-strategy",
|
||||
Description: "Legacy `strategy` DNS rule action option",
|
||||
DeprecatedVersion: "1.14.0",
|
||||
ScheduledVersion: "1.16.0",
|
||||
EnvName: "LEGACY_DNS_RULE_STRATEGY",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items",
|
||||
}
|
||||
|
||||
var Options = []Note{
|
||||
OptionLegacyDNSTransport,
|
||||
OptionLegacyDNSFakeIPOptions,
|
||||
OptionOutboundDNSRuleItem,
|
||||
OptionMissingDomainResolver,
|
||||
OptionLegacyDomainStrategyOptions,
|
||||
OptionInlineACME,
|
||||
OptionIPAcceptAny,
|
||||
OptionRuleSetIPCIDRAcceptEmpty,
|
||||
OptionLegacyDNSAddressFilter,
|
||||
OptionLegacyDNSRuleStrategy,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"runtime"
|
||||
_ "runtime/pprof"
|
||||
"unsafe"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"unsafe"
|
||||
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
|
||||
272
option/dns.go
272
option/dns.go
@@ -3,19 +3,14 @@ package option
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type RawDNSOptions struct {
|
||||
@@ -26,80 +21,29 @@ type RawDNSOptions struct {
|
||||
DNSClientOptions
|
||||
}
|
||||
|
||||
type LegacyDNSOptions struct {
|
||||
FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"`
|
||||
}
|
||||
|
||||
type DNSOptions struct {
|
||||
RawDNSOptions
|
||||
LegacyDNSOptions
|
||||
}
|
||||
|
||||
type contextKeyDontUpgrade struct{}
|
||||
const (
|
||||
legacyDNSFakeIPRemovedMessage = "legacy DNS fakeip options are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
|
||||
legacyDNSServerRemovedMessage = "legacy DNS server formats are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-to-new-dns-server-formats"
|
||||
)
|
||||
|
||||
func ContextWithDontUpgrade(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, (*contextKeyDontUpgrade)(nil), true)
|
||||
}
|
||||
|
||||
func dontUpgradeFromContext(ctx context.Context) bool {
|
||||
return ctx.Value((*contextKeyDontUpgrade)(nil)) == true
|
||||
type removedLegacyDNSOptions struct {
|
||||
FakeIP json.RawMessage `json:"fakeip,omitempty"`
|
||||
}
|
||||
|
||||
func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
|
||||
err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions)
|
||||
var legacyOptions removedLegacyDNSOptions
|
||||
err := json.UnmarshalContext(ctx, content, &legacyOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dontUpgrade := dontUpgradeFromContext(ctx)
|
||||
legacyOptions := o.LegacyDNSOptions
|
||||
if !dontUpgrade {
|
||||
if o.FakeIP != nil && o.FakeIP.Enabled {
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyDNSFakeIPOptions)
|
||||
ctx = context.WithValue(ctx, (*LegacyDNSFakeIPOptions)(nil), o.FakeIP)
|
||||
}
|
||||
o.LegacyDNSOptions = LegacyDNSOptions{}
|
||||
if len(legacyOptions.FakeIP) != 0 {
|
||||
return E.New(legacyDNSFakeIPRemovedMessage)
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dontUpgrade {
|
||||
rcodeMap := make(map[string]int)
|
||||
o.Servers = common.Filter(o.Servers, func(it DNSServerOptions) bool {
|
||||
if it.Type == C.DNSTypeLegacyRcode {
|
||||
rcodeMap[it.Tag] = it.Options.(int)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(rcodeMap) > 0 {
|
||||
for i := 0; i < len(o.Rules); i++ {
|
||||
rewriteRcode(rcodeMap, &o.Rules[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func rewriteRcode(rcodeMap map[string]int, rule *DNSRule) {
|
||||
switch rule.Type {
|
||||
case C.RuleTypeDefault:
|
||||
rewriteRcodeAction(rcodeMap, &rule.DefaultOptions.DNSRuleAction)
|
||||
case C.RuleTypeLogical:
|
||||
rewriteRcodeAction(rcodeMap, &rule.LogicalOptions.DNSRuleAction)
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteRcodeAction(rcodeMap map[string]int, ruleAction *DNSRuleAction) {
|
||||
if ruleAction.Action != C.RuleActionTypeRoute {
|
||||
return
|
||||
}
|
||||
rcode, loaded := rcodeMap[ruleAction.RouteOptions.Server]
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
ruleAction.Action = C.RuleActionTypePredefined
|
||||
ruleAction.PredefinedOptions.Rcode = common.Ptr(DNSRCode(rcode))
|
||||
return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
|
||||
}
|
||||
|
||||
type DNSClientOptions struct {
|
||||
@@ -111,12 +55,6 @@ type DNSClientOptions struct {
|
||||
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
|
||||
}
|
||||
|
||||
type LegacyDNSFakeIPOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Inet4Range *badoption.Prefix `json:"inet4_range,omitempty"`
|
||||
Inet6Range *badoption.Prefix `json:"inet6_range,omitempty"`
|
||||
}
|
||||
|
||||
type DNSTransportOptionsRegistry interface {
|
||||
CreateOptions(transportType string) (any, bool)
|
||||
}
|
||||
@@ -129,10 +67,6 @@ type _DNSServerOptions struct {
|
||||
type DNSServerOptions _DNSServerOptions
|
||||
|
||||
func (o *DNSServerOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
|
||||
switch o.Type {
|
||||
case C.DNSTypeLegacy:
|
||||
o.Type = ""
|
||||
}
|
||||
return badjson.MarshallObjectsContext(ctx, (*_DNSServerOptions)(o), o.Options)
|
||||
}
|
||||
|
||||
@@ -148,9 +82,7 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
|
||||
var options any
|
||||
switch o.Type {
|
||||
case "", C.DNSTypeLegacy:
|
||||
o.Type = C.DNSTypeLegacy
|
||||
options = new(LegacyDNSServerOptions)
|
||||
deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport)
|
||||
return E.New(legacyDNSServerRemovedMessage)
|
||||
default:
|
||||
var loaded bool
|
||||
options, loaded = registry.CreateOptions(o.Type)
|
||||
@@ -163,169 +95,6 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
|
||||
return err
|
||||
}
|
||||
o.Options = options
|
||||
if o.Type == C.DNSTypeLegacy && !dontUpgradeFromContext(ctx) {
|
||||
err = o.Upgrade(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
|
||||
if o.Type != C.DNSTypeLegacy {
|
||||
return nil
|
||||
}
|
||||
options := o.Options.(*LegacyDNSServerOptions)
|
||||
serverURL, _ := url.Parse(options.Address)
|
||||
var serverType string
|
||||
if serverURL != nil && serverURL.Scheme != "" {
|
||||
serverType = serverURL.Scheme
|
||||
} else {
|
||||
switch options.Address {
|
||||
case "local", "fakeip":
|
||||
serverType = options.Address
|
||||
default:
|
||||
serverType = C.DNSTypeUDP
|
||||
}
|
||||
}
|
||||
remoteOptions := RemoteDNSServerOptions{
|
||||
RawLocalDNSServerOptions: RawLocalDNSServerOptions{
|
||||
DialerOptions: DialerOptions{
|
||||
Detour: options.Detour,
|
||||
DomainResolver: &DomainResolveOptions{
|
||||
Server: options.AddressResolver,
|
||||
Strategy: options.AddressStrategy,
|
||||
},
|
||||
FallbackDelay: options.AddressFallbackDelay,
|
||||
},
|
||||
Legacy: true,
|
||||
LegacyStrategy: options.Strategy,
|
||||
LegacyDefaultDialer: options.Detour == "",
|
||||
LegacyClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||
},
|
||||
LegacyAddressResolver: options.AddressResolver,
|
||||
LegacyAddressStrategy: options.AddressStrategy,
|
||||
LegacyAddressFallbackDelay: options.AddressFallbackDelay,
|
||||
}
|
||||
switch serverType {
|
||||
case C.DNSTypeLocal:
|
||||
o.Type = C.DNSTypeLocal
|
||||
o.Options = &LocalDNSServerOptions{
|
||||
RawLocalDNSServerOptions: remoteOptions.RawLocalDNSServerOptions,
|
||||
}
|
||||
case C.DNSTypeUDP:
|
||||
o.Type = C.DNSTypeUDP
|
||||
o.Options = &remoteOptions
|
||||
var serverAddr M.Socksaddr
|
||||
if serverURL == nil || serverURL.Scheme == "" {
|
||||
serverAddr = M.ParseSocksaddr(options.Address)
|
||||
} else {
|
||||
serverAddr = M.ParseSocksaddr(serverURL.Host)
|
||||
}
|
||||
if !serverAddr.IsValid() {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
remoteOptions.Server = serverAddr.AddrString()
|
||||
if serverAddr.Port != 0 && serverAddr.Port != 53 {
|
||||
remoteOptions.ServerPort = serverAddr.Port
|
||||
}
|
||||
case C.DNSTypeTCP:
|
||||
o.Type = C.DNSTypeTCP
|
||||
o.Options = &remoteOptions
|
||||
if serverURL == nil {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
serverAddr := M.ParseSocksaddr(serverURL.Host)
|
||||
if !serverAddr.IsValid() {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
remoteOptions.Server = serverAddr.AddrString()
|
||||
if serverAddr.Port != 0 && serverAddr.Port != 53 {
|
||||
remoteOptions.ServerPort = serverAddr.Port
|
||||
}
|
||||
case C.DNSTypeTLS, C.DNSTypeQUIC:
|
||||
o.Type = serverType
|
||||
if serverURL == nil {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
serverAddr := M.ParseSocksaddr(serverURL.Host)
|
||||
if !serverAddr.IsValid() {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
remoteOptions.Server = serverAddr.AddrString()
|
||||
if serverAddr.Port != 0 && serverAddr.Port != 853 {
|
||||
remoteOptions.ServerPort = serverAddr.Port
|
||||
}
|
||||
o.Options = &RemoteTLSDNSServerOptions{
|
||||
RemoteDNSServerOptions: remoteOptions,
|
||||
}
|
||||
case C.DNSTypeHTTPS, C.DNSTypeHTTP3:
|
||||
o.Type = serverType
|
||||
httpsOptions := RemoteHTTPSDNSServerOptions{
|
||||
RemoteTLSDNSServerOptions: RemoteTLSDNSServerOptions{
|
||||
RemoteDNSServerOptions: remoteOptions,
|
||||
},
|
||||
}
|
||||
o.Options = &httpsOptions
|
||||
if serverURL == nil {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
serverAddr := M.ParseSocksaddr(serverURL.Host)
|
||||
if !serverAddr.IsValid() {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
httpsOptions.Server = serverAddr.AddrString()
|
||||
if serverAddr.Port != 0 && serverAddr.Port != 443 {
|
||||
httpsOptions.ServerPort = serverAddr.Port
|
||||
}
|
||||
if serverURL.Path != "/dns-query" {
|
||||
httpsOptions.Path = serverURL.Path
|
||||
}
|
||||
case "rcode":
|
||||
var rcode int
|
||||
if serverURL == nil {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
switch serverURL.Host {
|
||||
case "success":
|
||||
rcode = dns.RcodeSuccess
|
||||
case "format_error":
|
||||
rcode = dns.RcodeFormatError
|
||||
case "server_failure":
|
||||
rcode = dns.RcodeServerFailure
|
||||
case "name_error":
|
||||
rcode = dns.RcodeNameError
|
||||
case "not_implemented":
|
||||
rcode = dns.RcodeNotImplemented
|
||||
case "refused":
|
||||
rcode = dns.RcodeRefused
|
||||
default:
|
||||
return E.New("unknown rcode: ", serverURL.Host)
|
||||
}
|
||||
o.Type = C.DNSTypeLegacyRcode
|
||||
o.Options = rcode
|
||||
case C.DNSTypeDHCP:
|
||||
o.Type = C.DNSTypeDHCP
|
||||
dhcpOptions := DHCPDNSServerOptions{}
|
||||
if serverURL == nil {
|
||||
return E.New("invalid server address")
|
||||
}
|
||||
if serverURL.Host != "" && serverURL.Host != "auto" {
|
||||
dhcpOptions.Interface = serverURL.Host
|
||||
}
|
||||
o.Options = &dhcpOptions
|
||||
case C.DNSTypeFakeIP:
|
||||
o.Type = C.DNSTypeFakeIP
|
||||
fakeipOptions := FakeIPDNSServerOptions{}
|
||||
if legacyOptions, loaded := ctx.Value((*LegacyDNSFakeIPOptions)(nil)).(*LegacyDNSFakeIPOptions); loaded {
|
||||
fakeipOptions.Inet4Range = legacyOptions.Inet4Range
|
||||
fakeipOptions.Inet6Range = legacyOptions.Inet6Range
|
||||
}
|
||||
o.Options = &fakeipOptions
|
||||
default:
|
||||
return E.New("unsupported DNS server scheme: ", serverType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -350,16 +119,6 @@ func (o *DNSServerAddressOptions) ReplaceServerOptions(options ServerOptions) {
|
||||
*o = DNSServerAddressOptions(options)
|
||||
}
|
||||
|
||||
type LegacyDNSServerOptions struct {
|
||||
Address string `json:"address"`
|
||||
AddressResolver string `json:"address_resolver,omitempty"`
|
||||
AddressStrategy DomainStrategy `json:"address_strategy,omitempty"`
|
||||
AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"`
|
||||
Strategy DomainStrategy `json:"strategy,omitempty"`
|
||||
Detour string `json:"detour,omitempty"`
|
||||
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
|
||||
}
|
||||
|
||||
type HostsDNSServerOptions struct {
|
||||
Path badoption.Listable[string] `json:"path,omitempty"`
|
||||
Predefined *badjson.TypedMap[string, badoption.Listable[netip.Addr]] `json:"predefined,omitempty"`
|
||||
@@ -367,10 +126,6 @@ type HostsDNSServerOptions struct {
|
||||
|
||||
type RawLocalDNSServerOptions struct {
|
||||
DialerOptions
|
||||
Legacy bool `json:"-"`
|
||||
LegacyStrategy DomainStrategy `json:"-"`
|
||||
LegacyDefaultDialer bool `json:"-"`
|
||||
LegacyClientSubnet netip.Prefix `json:"-"`
|
||||
}
|
||||
|
||||
type LocalDNSServerOptions struct {
|
||||
@@ -381,9 +136,6 @@ type LocalDNSServerOptions struct {
|
||||
type RemoteDNSServerOptions struct {
|
||||
RawLocalDNSServerOptions
|
||||
DNSServerAddressOptions
|
||||
LegacyAddressResolver string `json:"-"`
|
||||
LegacyAddressStrategy DomainStrategy `json:"-"`
|
||||
LegacyAddressFallbackDelay badoption.Duration `json:"-"`
|
||||
}
|
||||
|
||||
type RemoteTLSDNSServerOptions struct {
|
||||
|
||||
@@ -2,6 +2,7 @@ package option
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const defaultDNSRecordTTL uint32 = 3600
|
||||
|
||||
type DNSRCode int
|
||||
|
||||
func (r DNSRCode) MarshalJSON() ([]byte, error) {
|
||||
@@ -76,10 +79,13 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
|
||||
if err == nil {
|
||||
return o.unmarshalBase64(binary)
|
||||
}
|
||||
record, err := dns.NewRR(stringValue)
|
||||
record, err := parseDNSRecord(stringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record == nil {
|
||||
return E.New("empty DNS record")
|
||||
}
|
||||
if a, isA := record.(*dns.A); isA {
|
||||
a.A = M.AddrFromIP(a.A).Unmap().AsSlice()
|
||||
}
|
||||
@@ -87,6 +93,16 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseDNSRecord(stringValue string) (dns.RR, error) {
|
||||
if len(stringValue) > 0 && stringValue[len(stringValue)-1] != '\n' {
|
||||
stringValue += "\n"
|
||||
}
|
||||
parser := dns.NewZoneParser(strings.NewReader(stringValue), "", "")
|
||||
parser.SetDefaultTTL(defaultDNSRecordTTL)
|
||||
record, _ := parser.Next()
|
||||
return record, parser.Err()
|
||||
}
|
||||
|
||||
func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
|
||||
record, _, err := dns.UnpackRR(binary, 0)
|
||||
if err != nil {
|
||||
@@ -100,3 +116,10 @@ func (o *DNSRecordOptions) unmarshalBase64(binary []byte) error {
|
||||
func (o DNSRecordOptions) Build() dns.RR {
|
||||
return o.RR
|
||||
}
|
||||
|
||||
func (o DNSRecordOptions) Match(record dns.RR) bool {
|
||||
if o.RR == nil || record == nil {
|
||||
return false
|
||||
}
|
||||
return dns.IsDuplicate(o.RR, record)
|
||||
}
|
||||
|
||||
40
option/dns_record_test.go
Normal file
40
option/dns_record_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func mustRecordOptions(t *testing.T, record string) DNSRecordOptions {
|
||||
t.Helper()
|
||||
var value DNSRecordOptions
|
||||
require.NoError(t, value.UnmarshalJSON([]byte(`"`+record+`"`)))
|
||||
return value
|
||||
}
|
||||
|
||||
func TestDNSRecordOptionsUnmarshalJSONRejectsRelativeNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, record := range []string{
|
||||
"@ IN A 1.1.1.1",
|
||||
"www IN CNAME example.com.",
|
||||
"example.com. IN CNAME @",
|
||||
"example.com. IN CNAME www",
|
||||
} {
|
||||
var value DNSRecordOptions
|
||||
err := value.UnmarshalJSON([]byte(`"` + record + `"`))
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSRecordOptionsMatchIgnoresTTL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
expected := mustRecordOptions(t, "example.com. 600 IN A 1.1.1.1")
|
||||
record, err := dns.NewRR("example.com. 60 IN A 1.1.1.1")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, expected.Match(record))
|
||||
}
|
||||
54
option/dns_test.go
Normal file
54
option/dns_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type stubDNSTransportOptionsRegistry struct{}
|
||||
|
||||
func (stubDNSTransportOptionsRegistry) CreateOptions(transportType string) (any, bool) {
|
||||
switch transportType {
|
||||
case C.DNSTypeUDP:
|
||||
return new(RemoteDNSServerOptions), true
|
||||
case C.DNSTypeFakeIP:
|
||||
return new(FakeIPDNSServerOptions), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSOptionsRejectsLegacyFakeIPOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
|
||||
var options DNSOptions
|
||||
err := json.UnmarshalContext(ctx, []byte(`{
|
||||
"fakeip": {
|
||||
"enabled": true,
|
||||
"inet4_range": "198.18.0.0/15"
|
||||
}
|
||||
}`), &options)
|
||||
require.EqualError(t, err, legacyDNSFakeIPRemovedMessage)
|
||||
}
|
||||
|
||||
func TestDNSServerOptionsRejectsLegacyFormats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
|
||||
testCases := []string{
|
||||
`{"address":"1.1.1.1"}`,
|
||||
`{"type":"legacy","address":"1.1.1.1"}`,
|
||||
}
|
||||
for _, content := range testCases {
|
||||
var options DNSServerOptions
|
||||
err := json.UnmarshalContext(ctx, []byte(content), &options)
|
||||
require.EqualError(t, err, legacyDNSServerRemovedMessage)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -33,26 +34,24 @@ func (r Rule) MarshalJSON() ([]byte, error) {
|
||||
return badjson.MarshallObjects((_Rule)(r), v)
|
||||
}
|
||||
|
||||
func (r *Rule) UnmarshalJSON(bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_Rule)(r))
|
||||
func (r *Rule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
|
||||
err := json.UnmarshalContext(ctx, bytes, (*_Rule)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := rulePayloadWithoutType(ctx, bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var v any
|
||||
switch r.Type {
|
||||
case "", C.RuleTypeDefault:
|
||||
r.Type = C.RuleTypeDefault
|
||||
v = &r.DefaultOptions
|
||||
return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions)
|
||||
case C.RuleTypeLogical:
|
||||
v = &r.LogicalOptions
|
||||
return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions)
|
||||
default:
|
||||
return E.New("unknown rule type: " + r.Type)
|
||||
}
|
||||
err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Rule) IsValid() bool {
|
||||
@@ -160,6 +159,64 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error {
|
||||
return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction)
|
||||
}
|
||||
|
||||
func rulePayloadWithoutType(ctx context.Context, data []byte) ([]byte, error) {
|
||||
var content badjson.JSONObject
|
||||
err := content.UnmarshalJSONContext(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Remove("type")
|
||||
return content.MarshalJSONContext(ctx)
|
||||
}
|
||||
|
||||
func unmarshalDefaultRuleContext(ctx context.Context, data []byte, rule *DefaultRule) error {
|
||||
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rejectNestedRouteRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth := nestedRuleDepth(ctx)
|
||||
err = json.UnmarshalContext(ctx, data, &rule.RawDefaultRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawDefaultRule, &rule.RuleAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
|
||||
rule.RuleAction = RuleAction{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalLogicalRuleContext(ctx context.Context, data []byte, rule *LogicalRule) error {
|
||||
rawAction, routeOptions, err := inspectRouteRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rejectNestedRouteRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth := nestedRuleDepth(ctx)
|
||||
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &rule.RawLogicalRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, data, &rule.RawLogicalRule, &rule.RuleAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 0 && rawAction == "" && routeOptions == (RouteActionOptions{}) {
|
||||
rule.RuleAction = RuleAction{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LogicalRule) IsValid() bool {
|
||||
return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,10 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
|
||||
case C.RuleActionTypeRoute:
|
||||
r.Action = ""
|
||||
v = r.RouteOptions
|
||||
case C.RuleActionTypeEvaluate:
|
||||
v = r.RouteOptions
|
||||
case C.RuleActionTypeRespond:
|
||||
v = nil
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
v = r.RouteOptionsOptions
|
||||
case C.RuleActionTypeReject:
|
||||
@@ -124,6 +128,9 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
|
||||
default:
|
||||
return nil, E.New("unknown DNS rule action: " + r.Action)
|
||||
}
|
||||
if v == nil {
|
||||
return badjson.MarshallObjects((_DNSRuleAction)(r))
|
||||
}
|
||||
return badjson.MarshallObjects((_DNSRuleAction)(r), v)
|
||||
}
|
||||
|
||||
@@ -137,6 +144,10 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
|
||||
case "", C.RuleActionTypeRoute:
|
||||
r.Action = C.RuleActionTypeRoute
|
||||
v = &r.RouteOptions
|
||||
case C.RuleActionTypeEvaluate:
|
||||
v = &r.RouteOptions
|
||||
case C.RuleActionTypeRespond:
|
||||
v = nil
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
v = &r.RouteOptionsOptions
|
||||
case C.RuleActionTypeReject:
|
||||
@@ -146,6 +157,9 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e
|
||||
default:
|
||||
return E.New("unknown DNS rule action: " + r.Action)
|
||||
}
|
||||
if v == nil {
|
||||
return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{})
|
||||
}
|
||||
return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v)
|
||||
}
|
||||
|
||||
|
||||
29
option/rule_action_test.go
Normal file
29
option/rule_action_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDNSRuleActionRespondUnmarshalJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var action DNSRuleAction
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond"}`), &action)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.RuleActionTypeRespond, action.Action)
|
||||
require.Equal(t, DNSRouteActionOptions{}, action.RouteOptions)
|
||||
}
|
||||
|
||||
func TestDNSRuleActionRespondRejectsUnknownFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var action DNSRuleAction
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{"action":"respond","disable_cache":true}`), &action)
|
||||
require.ErrorContains(t, err, "unknown field")
|
||||
}
|
||||
@@ -35,7 +35,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
|
||||
err := json.Unmarshal(bytes, (*_DNSRule)(r))
|
||||
err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -78,12 +78,6 @@ type RawDefaultDNSRule struct {
|
||||
DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"`
|
||||
DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"`
|
||||
DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"`
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
||||
SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"`
|
||||
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
|
||||
SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"`
|
||||
@@ -110,9 +104,23 @@ type RawDefaultDNSRule struct {
|
||||
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||
MatchResponse bool `json:"match_response,omitempty"`
|
||||
IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"`
|
||||
IPIsPrivate bool `json:"ip_is_private,omitempty"`
|
||||
ResponseRcode *DNSRCode `json:"response_rcode,omitempty"`
|
||||
ResponseAnswer badoption.Listable[DNSRecordOptions] `json:"response_answer,omitempty"`
|
||||
ResponseNs badoption.Listable[DNSRecordOptions] `json:"response_ns,omitempty"`
|
||||
ResponseExtra badoption.Listable[DNSRecordOptions] `json:"response_extra,omitempty"`
|
||||
Invert bool `json:"invert,omitempty"`
|
||||
|
||||
// Deprecated: removed in sing-box 1.12.0
|
||||
Geosite badoption.Listable[string] `json:"geosite,omitempty"`
|
||||
SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"`
|
||||
GeoIP badoption.Listable[string] `json:"geoip,omitempty"`
|
||||
// Deprecated: use match_response with response items
|
||||
IPAcceptAny bool `json:"ip_accept_any,omitempty"`
|
||||
// Deprecated: removed in sing-box 1.11.0
|
||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||
// Deprecated: renamed to rule_set_ip_cidr_match_source
|
||||
Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"`
|
||||
}
|
||||
@@ -127,11 +135,27 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
|
||||
err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
|
||||
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
|
||||
err = rejectNestedDNSRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth := nestedRuleDepth(ctx)
|
||||
err = json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
|
||||
r.DNSRuleAction = DNSRuleAction{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r DefaultDNSRule) IsValid() bool {
|
||||
@@ -156,11 +180,27 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
|
||||
err := json.Unmarshal(data, &r.RawLogicalDNSRule)
|
||||
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
|
||||
err = rejectNestedDNSRuleAction(ctx, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth := nestedRuleDepth(ctx)
|
||||
err = json.UnmarshalContext(nestedRuleChildContext(ctx), data, &r.RawLogicalDNSRule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if depth > 0 && rawAction == "" && routeOptions == (DNSRouteActionOptions{}) {
|
||||
r.DNSRuleAction = DNSRuleAction{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) IsValid() bool {
|
||||
|
||||
133
option/rule_nested.go
Normal file
133
option/rule_nested.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
)
|
||||
|
||||
type nestedRuleDepthContextKey struct{}
|
||||
|
||||
const (
|
||||
RouteRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules"
|
||||
DNSRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules"
|
||||
)
|
||||
|
||||
var (
|
||||
routeRuleActionKeys = jsonFieldNames(reflect.TypeFor[_RuleAction](), reflect.TypeFor[RouteActionOptions]())
|
||||
dnsRuleActionKeys = jsonFieldNames(reflect.TypeFor[_DNSRuleAction](), reflect.TypeFor[DNSRouteActionOptions]())
|
||||
)
|
||||
|
||||
func nestedRuleChildContext(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, nestedRuleDepthContextKey{}, nestedRuleDepth(ctx)+1)
|
||||
}
|
||||
|
||||
func rejectNestedRouteRuleAction(ctx context.Context, content []byte) error {
|
||||
return rejectNestedRuleAction(ctx, content, routeRuleActionKeys, RouteRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func rejectNestedDNSRuleAction(ctx context.Context, content []byte) error {
|
||||
return rejectNestedRuleAction(ctx, content, dnsRuleActionKeys, DNSRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func nestedRuleDepth(ctx context.Context) int {
|
||||
depth, _ := ctx.Value(nestedRuleDepthContextKey{}).(int)
|
||||
return depth
|
||||
}
|
||||
|
||||
func rejectNestedRuleAction(ctx context.Context, content []byte, keys []string, message string) error {
|
||||
if nestedRuleDepth(ctx) == 0 {
|
||||
return nil
|
||||
}
|
||||
hasActionKey, err := hasAnyJSONKey(ctx, content, keys...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActionKey {
|
||||
return E.New(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasAnyJSONKey(ctx context.Context, content []byte, keys ...string) (bool, error) {
|
||||
var object badjson.JSONObject
|
||||
err := object.UnmarshalJSONContext(ctx, content)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, key := range keys {
|
||||
if object.ContainsKey(key) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func inspectRouteRuleAction(ctx context.Context, content []byte) (string, RouteActionOptions, error) {
|
||||
var rawAction _RuleAction
|
||||
err := json.UnmarshalContext(ctx, content, &rawAction)
|
||||
if err != nil {
|
||||
return "", RouteActionOptions{}, err
|
||||
}
|
||||
var routeOptions RouteActionOptions
|
||||
err = json.UnmarshalContext(ctx, content, &routeOptions)
|
||||
if err != nil {
|
||||
return "", RouteActionOptions{}, err
|
||||
}
|
||||
return rawAction.Action, routeOptions, nil
|
||||
}
|
||||
|
||||
func inspectDNSRuleAction(ctx context.Context, content []byte) (string, DNSRouteActionOptions, error) {
|
||||
var rawAction _DNSRuleAction
|
||||
err := json.UnmarshalContext(ctx, content, &rawAction)
|
||||
if err != nil {
|
||||
return "", DNSRouteActionOptions{}, err
|
||||
}
|
||||
var routeOptions DNSRouteActionOptions
|
||||
err = json.UnmarshalContext(ctx, content, &routeOptions)
|
||||
if err != nil {
|
||||
return "", DNSRouteActionOptions{}, err
|
||||
}
|
||||
return rawAction.Action, routeOptions, nil
|
||||
}
|
||||
|
||||
func jsonFieldNames(types ...reflect.Type) []string {
|
||||
fieldMap := make(map[string]struct{})
|
||||
for _, fieldType := range types {
|
||||
appendJSONFieldNames(fieldMap, fieldType)
|
||||
}
|
||||
fieldNames := make([]string, 0, len(fieldMap))
|
||||
for fieldName := range fieldMap {
|
||||
fieldNames = append(fieldNames, fieldName)
|
||||
}
|
||||
return fieldNames
|
||||
}
|
||||
|
||||
func appendJSONFieldNames(fieldMap map[string]struct{}, fieldType reflect.Type) {
|
||||
for fieldType.Kind() == reflect.Pointer {
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
if fieldType.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
for i := range fieldType.NumField() {
|
||||
field := fieldType.Field(i)
|
||||
tagValue := field.Tag.Get("json")
|
||||
tagName, _, _ := strings.Cut(tagValue, ",")
|
||||
if tagName == "-" {
|
||||
continue
|
||||
}
|
||||
if field.Anonymous && tagName == "" {
|
||||
appendJSONFieldNames(fieldMap, field.Type)
|
||||
continue
|
||||
}
|
||||
if tagName == "" {
|
||||
tagName = field.Name
|
||||
}
|
||||
fieldMap[tagName] = struct{}{}
|
||||
}
|
||||
}
|
||||
68
option/rule_nested_test.go
Normal file
68
option/rule_nested_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRuleRejectsNestedDefaultRuleAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rule Rule
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
"rules": [
|
||||
{"domain": "example.com", "outbound": "direct"}
|
||||
]
|
||||
}`), &rule)
|
||||
require.ErrorContains(t, err, RouteRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rule Rule
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
"rules": [
|
||||
{"domain": "example.com", "foo": "bar"}
|
||||
]
|
||||
}`), &rule)
|
||||
require.ErrorContains(t, err, "unknown field")
|
||||
require.NotContains(t, err.Error(), RouteRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestDNSRuleRejectsNestedDefaultRuleAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rule DNSRule
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
"rules": [
|
||||
{"domain": "example.com", "server": "default"}
|
||||
]
|
||||
}`), &rule)
|
||||
require.ErrorContains(t, err, DNSRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
|
||||
func TestDNSRuleLeavesUnknownNestedKeysToNormalValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var rule DNSRule
|
||||
err := json.UnmarshalContext(context.Background(), []byte(`{
|
||||
"type": "logical",
|
||||
"mode": "and",
|
||||
"rules": [
|
||||
{"domain": "example.com", "foo": "bar"}
|
||||
]
|
||||
}`), &rule)
|
||||
require.ErrorContains(t, err, "unknown field")
|
||||
require.NotContains(t, err.Error(), DNSRuleActionNestedUnsupportedMessage)
|
||||
}
|
||||
@@ -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, "]")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
26
route/rule/rule_item_response_rcode.go
Normal file
26
route/rule/rule_item_response_rcode.go
Normal file
@@ -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])
|
||||
}
|
||||
63
route/rule/rule_item_response_record.go
Normal file
63
route/rule/rule_item_response_record.go
Normal file
@@ -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()
|
||||
}
|
||||
|
||||
138
route/rule/rule_item_rule_set_test.go
Normal file
138
route/rule/rule_item_rule_set_test.go
Normal file
@@ -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())
|
||||
}
|
||||
71
route/rule/rule_nested_action.go
Normal file
71
route/rule/rule_nested_action.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
88
route/rule/rule_nested_action_test.go
Normal file
88
route/rule/rule_nested_action_test.go
Normal file
@@ -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)
|
||||
|
||||
111
route/rule/rule_set_update_validation_test.go
Normal file
111
route/rule/rule_set_update_validation_test.go
Normal file
@@ -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