Compare commits

..

9 Commits

Author SHA1 Message Date
世界
b1b6bf07ae draft: Windows auto redirect 2026-03-27 21:39:48 +08:00
世界
fd09582c6a platform: Add HTTPResponse.WriteToWithProgress 2026-03-26 16:49:28 +08:00
世界
6c55bbd921 Bump version 2026-03-26 16:44:03 +08:00
nekohasekai
2e15cf82b2 Refactor ACME support to certificate provider 2026-03-26 16:44:03 +08:00
世界
6a7fe70ee8 cronet-go: Update chromium to 145.0.7632.159 2026-03-26 16:44:02 +08:00
世界
a6e4184252 documentation: Update descriptions for neighbor rules 2026-03-26 16:44:02 +08:00
世界
83b19121da Add macOS support for MAC and hostname rule items 2026-03-26 16:44:02 +08:00
世界
ddf24c2154 Add Android support for MAC and hostname rule items 2026-03-26 16:44:02 +08:00
世界
ede12fa117 Add MAC and hostname rule items 2026-03-26 16:44:02 +08:00
79 changed files with 703 additions and 6686 deletions

View File

@@ -4,7 +4,6 @@
--license GPL-3.0-or-later
--description "The universal proxy platform."
--url "https://sing-box.sagernet.org/"
--vendor SagerNet
--maintainer "nekohasekai <contact-git@sekai.icu>"
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes

View File

@@ -25,8 +25,8 @@ type DNSRouter interface {
type DNSClient interface {
Start()
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)
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)
ClearCache()
}
@@ -72,6 +72,11 @@ 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)

View File

@@ -10,8 +10,6 @@ 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 {
@@ -81,16 +79,14 @@ type InboundContext struct {
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DestinationAddresses []netip.Addr
DNSResponse *dns.Msg
DestinationAddressMatchFromResponse bool
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
// rule cache
@@ -119,46 +115,6 @@ func (c *InboundContext) ResetRuleMatchCache() {
c.DidMatch = false
}
func (c *InboundContext) DestinationAddressesForMatch() []netip.Addr {
if c.DestinationAddressMatchFromResponse {
return DNSResponseAddresses(c.DNSResponse)
}
return c.DestinationAddresses
}
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:
addresses = append(addresses, M.AddrFromIP(record.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(record.AAAA))
case *dns.HTTPS:
for _, value := range record.SVCB.Value {
switch hint := value.(type) {
case *dns.SVCBIPv4Hint:
for _, ip := range hint.Hint {
addresses = append(addresses, M.AddrFromIP(ip).Unmap())
}
case *dns.SVCBIPv6Hint:
for _, ip := range hint.Hint {
addresses = append(addresses, M.AddrFromIP(ip))
}
}
}
}
}
return addresses
}
type inboundContextKey struct{}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {

View File

@@ -1,45 +0,0 @@
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())
}

View File

@@ -66,14 +66,10 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
// Rule-set metadata only exposes headless-rule capabilities that outer routers
// need before evaluating nested matches. Headless rules do not support
// ip_version, so there is intentionally no ContainsIPVersionRule flag here.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type HTTPStartContext struct {
ctx context.Context

View File

@@ -2,8 +2,6 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -20,9 +18,8 @@ type Rule interface {
type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleAction interface {

2
box.go
View File

@@ -486,7 +486,7 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
if err != nil {
return err
}

View File

@@ -15,18 +15,19 @@ const (
)
const (
DNSTypeLegacy = "legacy"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local"
DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
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"
)
const (

View File

@@ -29,7 +29,6 @@ const (
const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeEvaluate = "evaluate"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"

View File

@@ -5,6 +5,7 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -13,6 +14,7 @@ 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"
@@ -107,7 +109,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(response *dns.Msg) bool) (*dns.Msg, error) {
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) {
if len(message.Question) == 0 {
if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
@@ -237,10 +239,13 @@ 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(response)
rejected = !responseChecker(MessageToAddresses(response))
}
if rejected {
if !disableCache && c.rdrc != nil {
@@ -310,7 +315,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(response *dns.Msg) bool) ([]netip.Addr, error) {
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
var strategy C.DomainStrategy
@@ -395,7 +400,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(response *dns.Msg) bool) ([]netip.Addr, error) {
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) {
question := dns.Question{
Name: name,
Qtype: qType,
@@ -510,7 +515,25 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {
return adapter.DNSResponseAddresses(response)
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
}
func wrapError(err error) error {

View File

@@ -1,111 +0,0 @@
package dns
import (
"context"
"errors"
"net/netip"
"testing"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"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
},
})
addrs, 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")}, addrs)
}
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, errors.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))
}

View File

@@ -5,13 +5,11 @@ 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"
@@ -21,8 +19,6 @@ 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/common/x/list"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
"github.com/sagernet/sing/service"
@@ -32,29 +28,16 @@ import (
var _ adapter.DNSRouter = (*Router)(nil)
type dnsRuleSetCallback struct {
ruleSet adapter.RuleSet
element *list.Element[adapter.RuleSetUpdateCallback]
}
type Router struct {
ctx context.Context
logger logger.ContextLogger
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
rebuildAccess sync.Mutex
closing bool
ruleSetCallbacks []dnsRuleSetCallback
addressFilterDeprecatedReported bool
ruleStrategyDeprecatedReported bool
ctx context.Context
logger logger.ContextLogger
transport adapter.DNSTransportManager
outbound adapter.OutboundManager
client adapter.DNSClient
rules []adapter.DNSRule
defaultDomainStrategy C.DomainStrategy
dnsReverseMapping freelru.Cache[netip.Addr, string]
platformInterface adapter.PlatformInterface
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
@@ -63,7 +46,6 @@ 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),
}
@@ -92,12 +74,13 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
}
func (r *Router) Initialize(rules []option.DNSRule) error {
r.rawRules = append(r.rawRules[:0], rules...)
newRules, _, err := r.buildRules(false)
if err != nil {
return err
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)
}
closeRules(newRules)
return nil
}
@@ -109,24 +92,12 @@ func (r *Router) Start(stage adapter.StartStage) error {
r.client.Start()
monitor.Finish()
monitor.Start("initialize DNS rules")
err := r.rebuildRules(true)
monitor.Finish()
if err != nil {
return err
}
monitor.Start("register DNS rule-set callbacks")
needsRulesRefresh, err := r.registerRuleSetCallbacks()
monitor.Finish()
if err != nil {
return err
}
if needsRulesRefresh {
monitor.Start("refresh DNS rules after callback registration")
err = r.rebuildRules(true)
for i, rule := range r.rules {
monitor.Start("initialize DNS rule[", i, "]")
err := rule.Start()
monitor.Finish()
if err != nil {
r.logger.Error(E.Cause(err, "refresh DNS rules after callback registration"))
return E.Cause(err, "initialize DNS rule[", i, "]")
}
}
}
@@ -135,18 +106,8 @@ func (r *Router) Start(stage adapter.StartStage) error {
func (r *Router) Close() error {
monitor := taskmonitor.New(r.logger, C.StopTimeout)
r.rulesAccess.Lock()
r.closing = true
callbacks := r.ruleSetCallbacks
r.ruleSetCallbacks = nil
runtimeRules := r.rules
r.rules = nil
for _, callback := range callbacks {
callback.ruleSet.UnregisterCallback(callback.element)
}
r.rulesAccess.Unlock()
var err error
for i, rule := range runtimeRules {
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, "]")
@@ -156,151 +117,6 @@ func (r *Router) Close() error {
return err
}
func (r *Router) rebuildRules(startRules bool) error {
r.rebuildAccess.Lock()
defer r.rebuildAccess.Unlock()
if r.isClosing() {
return nil
}
newRules, legacyDNSMode, err := r.buildRules(startRules)
if err != nil {
if r.isClosing() {
return nil
}
return err
}
shouldReportAddressFilterDeprecated := startRules &&
legacyDNSMode &&
!r.addressFilterDeprecatedReported &&
common.Any(newRules, func(rule adapter.DNSRule) bool { return rule.WithAddressLimit() })
shouldReportRuleStrategyDeprecated := startRules &&
legacyDNSMode &&
!r.ruleStrategyDeprecatedReported &&
hasDNSRuleActionStrategy(r.rawRules)
r.rulesAccess.Lock()
if r.closing {
r.rulesAccess.Unlock()
closeRules(newRules)
return nil
}
oldRules := r.rules
r.rules = newRules
r.legacyDNSMode = legacyDNSMode
if shouldReportAddressFilterDeprecated {
r.addressFilterDeprecatedReported = true
}
if shouldReportRuleStrategyDeprecated {
r.ruleStrategyDeprecatedReported = true
}
r.rulesAccess.Unlock()
closeRules(oldRules)
if shouldReportAddressFilterDeprecated {
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSAddressFilter)
}
if shouldReportRuleStrategyDeprecated {
deprecated.Report(r.ctx, deprecated.OptionLegacyDNSRuleStrategy)
}
return nil
}
func (r *Router) isClosing() bool {
r.rulesAccess.RLock()
defer r.rulesAccess.RUnlock()
return r.closing
}
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, error) {
for i, ruleOptions := range r.rawRules {
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
if err != nil {
return nil, false, E.Cause(err, "parse dns rule[", i, "]")
}
}
router := service.FromContext[adapter.Router](r.ctx)
legacyDNSMode, err := resolveLegacyDNSMode(router, r.rawRules)
if err != nil {
return nil, false, err
}
if !legacyDNSMode {
err = validateLegacyDNSModeDisabledRules(r.rawRules)
if err != nil {
return nil, false, err
}
}
newRules := make([]adapter.DNSRule, 0, len(r.rawRules))
for i, ruleOptions := range r.rawRules {
dnsRule, err := R.NewDNSRule(r.ctx, r.logger, ruleOptions, true, legacyDNSMode)
if err != nil {
closeRules(newRules)
return nil, false, 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, E.Cause(err, "initialize DNS rule[", i, "]")
}
}
}
return newRules, legacyDNSMode, nil
}
func closeRules(rules []adapter.DNSRule) {
for _, rule := range rules {
_ = rule.Close()
}
}
func (r *Router) registerRuleSetCallbacks() (bool, error) {
tags := referencedDNSRuleSetTags(r.rawRules)
if len(tags) == 0 {
return false, nil
}
r.rulesAccess.RLock()
if len(r.ruleSetCallbacks) > 0 {
r.rulesAccess.RUnlock()
return true, nil
}
r.rulesAccess.RUnlock()
router := service.FromContext[adapter.Router](r.ctx)
if router == nil {
return false, E.New("router service not found")
}
callbacks := make([]dnsRuleSetCallback, 0, len(tags))
for _, tag := range tags {
ruleSet, loaded := router.RuleSet(tag)
if !loaded {
for _, callback := range callbacks {
callback.ruleSet.UnregisterCallback(callback.element)
}
return false, E.New("rule-set not found: ", tag)
}
element := ruleSet.RegisterCallback(func(adapter.RuleSet) {
err := r.rebuildRules(true)
if err != nil {
r.logger.Error(E.Cause(err, "rebuild DNS rules after rule-set update"))
}
})
callbacks = append(callbacks, dnsRuleSetCallback{
ruleSet: ruleSet,
element: element,
})
}
r.rulesAccess.Lock()
if len(r.ruleSetCallbacks) == 0 {
r.ruleSetCallbacks = callbacks
callbacks = nil
}
r.rulesAccess.Unlock()
for _, callback := range callbacks {
callback.ruleSet.UnregisterCallback(callback.element)
}
return true, nil
}
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, adapter.DNSRule, int) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
@@ -316,12 +132,16 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int,
continue
}
metadata.ResetRuleCache()
metadata.DestinationAddressMatchFromResponse = false
if currentRule.LegacyPreMatch(metadata) {
if ruleDescription := currentRule.String(); ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
if currentRule.Match(metadata) {
displayRuleIndex := currentRuleIndex
if displayRuleIndex != -1 {
displayRuleIndex += displayRuleIndex + 1
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action())
}
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRoute:
@@ -346,6 +166,14 @@ 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 {
@@ -368,281 +196,17 @@ 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()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
return transport, nil, -1
}
func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOptions R.RuleActionDNSRouteOptions) {
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(action *R.RuleActionDNSRoute, allowFakeIP bool, options *adapter.DNSQueryOptions) (adapter.DNSTransport, dnsRouteStatus) {
transport, loaded := r.transport.Transport(action.Server)
if !loaded {
return nil, dnsRouteStatusMissing
}
isFakeIP := transport.Type() == C.DNSTypeFakeIP
if isFakeIP && !allowFakeIP {
return transport, dnsRouteStatusSkipped
}
r.applyDNSRouteOptions(options, action.RuleActionDNSRouteOptions)
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
}
func (r *Router) exchangeWithRules(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions, allowFakeIP bool) exchangeWithRulesResult {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
}
effectiveOptions := options
var savedResponse *mDNS.Msg
for currentRuleIndex, currentRule := range r.rules {
metadata.ResetRuleCache()
metadata.DNSResponse = savedResponse
metadata.DestinationAddressMatchFromResponse = false
if !currentRule.Match(metadata) {
continue
}
r.logRuleMatch(ctx, currentRuleIndex, currentRule)
switch action := currentRule.Action().(type) {
case *R.RuleActionDNSRouteOptions:
r.applyDNSRouteOptions(&effectiveOptions, *action)
case *R.RuleActionEvaluate:
queryOptions := effectiveOptions
transport, status := r.resolveDNSRoute(&R.RuleActionDNSRoute{
Server: action.Server,
RuleActionDNSRouteOptions: action.RuleActionDNSRouteOptions,
}, allowFakeIP, &queryOptions)
switch status {
case dnsRouteStatusMissing:
r.logger.ErrorContext(ctx, "transport not found: ", action.Server)
savedResponse = nil
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)
if err != nil {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
savedResponse = nil
continue
}
savedResponse = response
case *R.RuleActionDNSRoute:
queryOptions := effectiveOptions
transport, status := r.resolveDNSRoute(action, 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),
}
}
}
queryOptions := effectiveOptions
transport := r.transport.Default()
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,
}
}
type lookupWithRulesResponse struct {
addresses []netip.Addr
}
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 lookupStrategyAllowsQueryType(strategy C.DomainStrategy, qType uint16) bool {
switch strategy {
case C.DomainStrategyIPv4Only:
return qType == mDNS.TypeA
case C.DomainStrategyIPv6Only:
return qType == mDNS.TypeAAAA
default:
return true
}
}
func 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, 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 {
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
return response.addresses, err
}
if strategy == C.DomainStrategyIPv6Only {
response, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
return response.addresses, err
}
var (
response4 lookupWithRulesResponse
response6 lookupWithRulesResponse
)
var group task.Group
group.Append("exchange4", func(ctx context.Context) error {
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeA, lookupOptions)
response4 = result
return err
})
group.Append("exchange6", func(ctx context.Context) error {
result, err := r.lookupWithRulesType(ctx, domain, mDNS.TypeAAAA, lookupOptions)
response6 = result
return err
})
err := group.Run(ctx)
if len(response4.addresses) == 0 && len(response6.addresses) == 0 {
return nil, err
}
return sortAddresses(response4.addresses, response6.addresses, strategy), nil
}
func (r *Router) lookupWithRulesType(ctx context.Context, domain string, qType uint16, options adapter.DNSQueryOptions) (lookupWithRulesResponse, error) {
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{{
Name: mDNS.Fqdn(FqdnToDomain(domain)),
Qtype: qType,
Qclass: mDNS.ClassINET,
}},
}
exchangeResult := r.exchangeWithRules(withLookupQueryMetadata(ctx, qType), request, options, false)
result := lookupWithRulesResponse{}
if exchangeResult.rejectAction != nil {
return result, exchangeResult.rejectAction.Error(ctx)
}
if exchangeResult.err != nil {
return result, exchangeResult.err
}
if exchangeResult.response.Rcode != mDNS.RcodeSuccess {
return result, RcodeError(exchangeResult.response.Rcode)
}
if !lookupStrategyAllowsQueryType(r.resolveLookupStrategy(options), qType) {
return result, nil
}
result.addresses = filterAddressesByQueryType(MessageToAddresses(exchangeResult.response), qType)
return result, nil
}
func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapter.DNSQueryOptions) (*mDNS.Msg, error) {
if len(message.Question) != 1 {
r.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
@@ -656,8 +220,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
}
return &responseMessage, nil
}
r.rulesAccess.RLock()
defer r.rulesAccess.RUnlock()
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var (
response *mDNS.Msg
@@ -668,8 +230,6 @@ 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
@@ -679,13 +239,18 @@ 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 !r.legacyDNSMode {
exchangeResult := r.exchangeWithRules(ctx, message, options, true)
response, transport, err = exchangeResult.response, exchangeResult.transport, exchangeResult.err
} else {
var (
rule adapter.DNSRule
@@ -760,8 +325,6 @@ 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()
var (
responseAddrs []netip.Addr
err error
@@ -775,8 +338,6 @@ 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))
}
@@ -789,16 +350,20 @@ 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 !r.legacyDNSMode {
responseAddrs, err = r.lookupWithRules(ctx, domain, options)
} else {
var (
transport adapter.DNSTransport
@@ -860,15 +425,15 @@ func isAddressQuery(message *mDNS.Msg) bool {
return false
}
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(response *mDNS.Msg) bool {
func addressLimitResponseCheck(rule adapter.DNSRule, metadata *adapter.InboundContext) func(responseAddrs []netip.Addr) bool {
if rule == nil || !rule.WithAddressLimit() {
return nil
}
responseMetadata := *metadata
return func(response *mDNS.Msg) bool {
return func(responseAddrs []netip.Addr) bool {
checkMetadata := responseMetadata
checkMetadata.DNSResponse = response
return rule.MatchAddressLimit(&checkMetadata, response)
checkMetadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(&checkMetadata)
}
}
@@ -893,238 +458,3 @@ 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.IPVersion > 0 ||
len(rule.QueryType) > 0
}
func resolveLegacyDNSMode(router adapter.Router, rules []option.DNSRule) (bool, error) {
legacyDNSModeDisabled, needsLegacyDNSMode, needsLegacyDNSModeFromStrategy, err := dnsRuleModeRequirements(router, rules)
if err != nil {
return false, err
}
if legacyDNSModeDisabled && needsLegacyDNSModeFromStrategy {
return false, E.New("DNS rule action strategy is only supported in legacyDNSMode")
}
if legacyDNSModeDisabled {
return false, nil
}
return needsLegacyDNSMode, nil
}
func dnsRuleModeRequirements(router adapter.Router, rules []option.DNSRule) (bool, bool, bool, error) {
var legacyDNSModeDisabled bool
var needsLegacyDNSMode bool
var needsLegacyDNSModeFromStrategy bool
for i, rule := range rules {
ruleLegacyDNSModeDisabled, ruleNeedsLegacyDNSMode, ruleNeedsLegacyDNSModeFromStrategy, err := dnsRuleModeRequirementsInRule(router, rule)
if err != nil {
return false, false, false, E.Cause(err, "dns rule[", i, "]")
}
legacyDNSModeDisabled = legacyDNSModeDisabled || ruleLegacyDNSModeDisabled
needsLegacyDNSMode = needsLegacyDNSMode || ruleNeedsLegacyDNSMode
needsLegacyDNSModeFromStrategy = needsLegacyDNSModeFromStrategy || ruleNeedsLegacyDNSModeFromStrategy
}
return legacyDNSModeDisabled, needsLegacyDNSMode, needsLegacyDNSModeFromStrategy, nil
}
func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule) (bool, bool, bool, error) {
switch rule.Type {
case "", C.RuleTypeDefault:
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions)
case C.RuleTypeLogical:
legacyDNSModeDisabled := dnsRuleActionType(rule) == C.RuleActionTypeEvaluate
needsLegacyDNSModeFromStrategy := dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction)
needsLegacyDNSMode := needsLegacyDNSModeFromStrategy
for i, subRule := range rule.LogicalOptions.Rules {
subLegacyDNSModeDisabled, subNeedsLegacyDNSMode, subNeedsLegacyDNSModeFromStrategy, err := dnsRuleModeRequirementsInRule(router, subRule)
if err != nil {
return false, false, false, E.Cause(err, "sub rule[", i, "]")
}
legacyDNSModeDisabled = legacyDNSModeDisabled || subLegacyDNSModeDisabled
needsLegacyDNSMode = needsLegacyDNSMode || subNeedsLegacyDNSMode
needsLegacyDNSModeFromStrategy = needsLegacyDNSModeFromStrategy || subNeedsLegacyDNSModeFromStrategy
}
return legacyDNSModeDisabled, needsLegacyDNSMode, needsLegacyDNSModeFromStrategy, nil
default:
return false, false, false, nil
}
}
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule) (bool, bool, bool, error) {
legacyDNSModeDisabled := defaultRuleDisablesLegacyDNSMode(rule)
needsLegacyDNSModeFromStrategy := dnsRuleActionHasStrategy(rule.DNSRuleAction)
needsLegacyDNSMode := defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || needsLegacyDNSModeFromStrategy
if len(rule.RuleSet) == 0 {
return legacyDNSModeDisabled, needsLegacyDNSMode, needsLegacyDNSModeFromStrategy, nil
}
if router == nil {
return false, false, false, E.New("router service not found")
}
for _, tag := range rule.RuleSet {
ruleSet, loaded := router.RuleSet(tag)
if !loaded {
return false, false, false, E.New("rule-set not found: ", tag)
}
metadata := ruleSet.Metadata()
// Rule sets are built from headless rules, so query_type is the only
// per-query DNS predicate they can contribute here. ip_version is not a
// headless-rule item and is therefore intentionally absent from metadata.
legacyDNSModeDisabled = legacyDNSModeDisabled || metadata.ContainsDNSQueryTypeRule
if !rule.RuleSetIPCIDRMatchSource && metadata.ContainsIPCIDRRule {
needsLegacyDNSMode = true
}
}
return legacyDNSModeDisabled, needsLegacyDNSMode, needsLegacyDNSModeFromStrategy, 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 {
for i, rule := range rules {
consumesResponse, err := validateLegacyDNSModeDisabledRuleTree(rule)
if err != nil {
return E.Cause(err, "validate dns rule[", i, "]")
}
action := dnsRuleActionType(rule)
if action == C.RuleActionTypeEvaluate && consumesResponse {
return E.New("dns rule[", i, "]: evaluate rule cannot consume response state")
}
}
return nil
}
func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) {
switch rule.Type {
case "", C.RuleTypeDefault:
return validateLegacyDNSModeDisabledDefaultRule(rule.DefaultOptions)
case C.RuleTypeLogical:
var consumesResponse bool
for i, subRule := range rule.LogicalOptions.Rules {
subConsumesResponse, err := validateLegacyDNSModeDisabledRuleTree(subRule)
if err != nil {
return false, E.Cause(err, "sub rule[", i, "]")
}
consumesResponse = consumesResponse || subConsumesResponse
}
return consumesResponse, nil
default:
return false, nil
}
}
func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) {
hasResponseRecords := hasResponseMatchFields(rule)
if hasResponseRecords && !rule.MatchResponse {
return false, E.New("response_* items require match_response")
}
if (len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse {
return false, E.New("ip_cidr and ip_is_private require match_response when legacyDNSMode is disabled")
}
// 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("ip_accept_any is removed when legacyDNSMode is disabled, use ip_cidr with match_response")
}
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
return false, E.New("rule_set_ip_cidr_accept_empty is removed when legacyDNSMode is disabled")
}
return rule.MatchResponse, nil
}
func hasDNSRuleActionStrategy(rules []option.DNSRule) bool {
for _, rule := range rules {
if dnsRuleHasActionStrategy(rule) {
return true
}
}
return false
}
func dnsRuleHasActionStrategy(rule option.DNSRule) bool {
switch rule.Type {
case "", C.RuleTypeDefault:
return dnsRuleActionHasStrategy(rule.DefaultOptions.DNSRuleAction)
case C.RuleTypeLogical:
if dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction) {
return true
}
return hasDNSRuleActionStrategy(rule.LogicalOptions.Rules)
default:
return false
}
}
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 ""
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,21 @@
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 {
@@ -27,6 +35,8 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
}
@@ -35,10 +45,15 @@ 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,
}
}
@@ -53,3 +68,11 @@ 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
}

View File

@@ -2,25 +2,104 @@ 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) {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
})
if options.LegacyDefaultDialer {
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
}
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
})
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,
})
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
}

View File

@@ -2,23 +2,6 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.8
* Add BBR profile and hop interval randomization for Hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
#### 1.14.0-alpha.8
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements

View File

@@ -1,10 +1,10 @@
---
icon: material/note-remove
icon: material/delete-clock
---
!!! failure "Removed in sing-box 1.14.0"
!!! failure "Deprecated in sing-box 1.12.0"
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-servers).
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
### Structure

View File

@@ -1,10 +1,10 @@
---
icon: material/note-remove
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.14.0 移除"
!!! failure "已在 sing-box 1.12.0 废弃"
旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 fake-ip 配置已废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
### 结构

View File

@@ -39,7 +39,7 @@ icon: material/alert-decagram
|----------|---------------------------------|
| `server` | List of [DNS Server](./server/) |
| `rules` | List of [DNS Rule](./rule/) |
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
| `fakeip` | [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 overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.

View File

@@ -88,6 +88,6 @@ LRU 缓存容量。
可以被 `servers.[].client_subnet``rules.[].client_subnet` 覆盖。
#### fakeip :material-note-remove:
#### fakeip
[FakeIP](./fakeip/) 设置。

View File

@@ -4,15 +4,8 @@ icon: material/alert-decagram
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [match_response](#match_response)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-delete-clock: [ip_accept_any](#ip_accept_any)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [source_hostname](#source_hostname)
!!! quote "Changes in sing-box 1.13.0"
@@ -101,6 +94,12 @@ 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
],
@@ -172,16 +171,7 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": 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": [],
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
@@ -190,9 +180,7 @@ 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"
@@ -489,17 +477,6 @@ 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 DNS response data
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
instead of only matching the original query.
Required for `response_rcode`, `response_answer`, `response_ns`, `response_extra` fields.
Also required for `ip_cidr` and `ip_is_private` when `legacyDNSMode` is disabled.
#### invert
Invert match result.
@@ -570,69 +547,24 @@ Match GeoIP with query response.
Match IP CIDR with query response.
When `legacyDNSMode` is disabled, `match_response` must be set to `true`.
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
When `legacyDNSMode` is disabled, `match_response` must be set to `true`.
#### 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.
Only supported in `legacyDNSMode`.
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.
Only supported in `legacyDNSMode`. Use `match_response` with response items instead.
Match any IP with query response.
### Response Fields
!!! question "Since sing-box 1.14.0"
Match fields for DNS response data. Require `match_response` to be set to `true`
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
#### 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

View File

@@ -4,15 +4,8 @@ icon: material/alert-decagram
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [match_response](#match_response)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-delete-clock: [ip_accept_any](#ip_accept_any)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [source_hostname](#source_hostname)
!!! quote "sing-box 1.13.0 中的更改"
@@ -101,6 +94,12 @@ 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
],
@@ -172,16 +171,7 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": 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": [],
"rule_set_ip_cidr_accept_empty": false,
"invert": false,
"outbound": [
"direct"
@@ -190,9 +180,6 @@ 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"
@@ -489,15 +476,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### match_response
!!! question "自 sing-box 1.14.0 起"
启用响应匹配。启用后,此规则将匹配 DNS 响应数据(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
`response_rcode``response_answer``response_ns``response_extra` 字段需要此选项。
`legacyDNSMode` 未启用时,`ip_cidr``ip_is_private` 也需要此选项。
#### invert
反选匹配结果。
@@ -569,69 +547,24 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
与查询响应匹配 IP CIDR。
`legacyDNSMode` 未启用时,`match_response` 必须设为 `true`
#### ip_is_private
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配非公开 IP。
`legacyDNSMode` 未启用时,`match_response` 必须设为 `true`
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除。
仅在 `legacyDNSMode` 中可用。请使用 `match_response` 和响应项替代。
匹配任意 IP。
#### 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 中被移除。
仅在 `legacyDNSMode` 中可用。
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 响应字段
!!! question "自 sing-box 1.14.0 起"
DNS 响应数据的匹配字段。需要将 `match_response` 设为 `true`
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
#### 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

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [evaluate](#evaluate)
:material-delete-clock: [strategy](#strategy)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [strategy](#strategy)
@@ -39,11 +34,7 @@ Tag of target server.
!!! question "Since sing-box 1.12.0"
!!! warning
`strategy` is deprecated and only supported in `legacyDNSMode`.
Set domain strategy for this query in `legacyDNSMode`.
Set domain strategy for this query.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
@@ -61,49 +52,7 @@ 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 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 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).
#### 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`.
Will overrides `dns.client_subnet`.
### route-options

View File

@@ -2,11 +2,6 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [evaluate](#evaluate)
:material-delete-clock: [strategy](#strategy)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [strategy](#strategy)
@@ -39,11 +34,7 @@ icon: material/new-box
!!! question "自 sing-box 1.12.0 起"
!!! warning
`strategy` 已废弃,且仅在 `legacyDNSMode` 中可用。
`legacyDNSMode` 中为此查询设置域名策略。
为此查询设置域名策略。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -63,46 +54,6 @@ 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 规则中使用(不可在逻辑子规则内部使用)。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
#### client_subnet
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet`.
### route-options
```json
@@ -133,7 +84,7 @@ icon: material/new-box
- `default`: 返回 REFUSED。
- `drop`: 丢弃请求。
默认使用 `default`
默认使用 `defualt`
#### no_drop

View File

@@ -29,7 +29,7 @@ The type of the DNS server.
| Type | Format |
|-----------------|---------------------------|
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
| empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |

View File

@@ -29,7 +29,7 @@ DNS 服务器的类型。
| 类型 | 格式 |
|-----------------|---------------------------|
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
| empty (default) | [Legacy](./legacy/) |
| `local` | [Local](./local/) |
| `hosts` | [Hosts](./hosts/) |
| `tcp` | [TCP](./tcp/) |

View File

@@ -1,10 +1,10 @@
---
icon: material/note-remove
icon: material/delete-clock
---
!!! failure "Removed in sing-box 1.14.0"
!!! failure "Deprecated in sing-box 1.12.0"
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-servers).
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
!!! 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 overridden by `rules.[].client_subnet`.
Can be overrides by `rules.[].client_subnet`.
Will override `dns.client_subnet`.
Will overrides `dns.client_subnet`.

View File

@@ -1,10 +1,10 @@
---
icon: material/note-remove
icon: material/delete-clock
---
!!! failure "已在 sing-box 1.14.0 移除"
!!! failure "Deprecated in sing-box 1.12.0"
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
旧的 DNS 服务器配置已废弃且在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
!!! quote "sing-box 1.9.0 中的更改"

View File

@@ -2,10 +2,6 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "Changes in sing-box 1.11.0"
:material-alert: [masquerade](#masquerade)
@@ -35,7 +31,6 @@ icon: material/alert-decagram
"ignore_client_bandwidth": false,
"tls": {},
"masquerade": "", // or {}
"bbr_profile": "",
"brutal_debug": false
}
```
@@ -146,14 +141,6 @@ Fixed response headers.
Fixed response content.
#### bbr_profile
!!! question "Since sing-box 1.14.0"
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
`standard` is used by default.
#### brutal_debug
Enable debug information logging for Hysteria Brutal CC.

View File

@@ -2,10 +2,6 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "sing-box 1.11.0 中的更改"
:material-alert: [masquerade](#masquerade)
@@ -35,7 +31,6 @@ icon: material/alert-decagram
"ignore_client_bandwidth": false,
"tls": {},
"masquerade": "", // 或 {}
"bbr_profile": "",
"brutal_debug": false
}
```
@@ -143,14 +138,6 @@ HTTP3 服务器认证失败时的行为 (对象配置)。
固定响应内容。
#### bbr_profile
!!! question "自 sing-box 1.14.0 起"
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`
默认使用 `standard`
#### brutal_debug
启用 Hysteria Brutal CC 的调试信息日志记录。

View File

@@ -1,8 +1,3 @@
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [hop_interval_max](#hop_interval_max)
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "Changes in sing-box 1.11.0"
:material-plus: [server_ports](#server_ports)
@@ -14,14 +9,13 @@
{
"type": "hysteria2",
"tag": "hy2-out",
"server": "127.0.0.1",
"server_port": 1080,
"server_ports": [
"2080:3000"
],
"hop_interval": "",
"hop_interval_max": "",
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
@@ -31,9 +25,8 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
"bbr_profile": "",
"brutal_debug": false,
... // Dial Fields
}
```
@@ -82,14 +75,6 @@ Port hopping interval.
`30s` is used by default.
#### hop_interval_max
!!! question "Since sing-box 1.14.0"
Maximum port hopping interval, used for randomization.
If set, the actual hop interval will be randomly chosen between `hop_interval` and `hop_interval_max`.
#### up_mbps, down_mbps
Max bandwidth, in Mbps.
@@ -124,14 +109,6 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
#### bbr_profile
!!! question "Since sing-box 1.14.0"
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
`standard` is used by default.
#### brutal_debug
Enable debug information logging for Hysteria Brutal CC.

View File

@@ -1,8 +1,3 @@
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [hop_interval_max](#hop_interval_max)
:material-plus: [bbr_profile](#bbr_profile)
!!! quote "sing-box 1.11.0 中的更改"
:material-plus: [server_ports](#server_ports)
@@ -21,7 +16,6 @@
"2080:3000"
],
"hop_interval": "",
"hop_interval_max": "",
"up_mbps": 100,
"down_mbps": 100,
"obfs": {
@@ -31,9 +25,8 @@
"password": "goofy_ahh_password",
"network": "tcp",
"tls": {},
"bbr_profile": "",
"brutal_debug": false,
... // 拨号字段
}
```
@@ -80,14 +73,6 @@
默认使用 `30s`
#### hop_interval_max
!!! question "自 sing-box 1.14.0 起"
最大端口跳跃间隔,用于随机化。
如果设置,实际跳跃间隔将在 `hop_interval``hop_interval_max` 之间随机选择。
#### up_mbps, down_mbps
最大带宽。
@@ -122,14 +107,6 @@ QUIC 流量混淆器密码.
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
#### bbr_profile
!!! question "自 sing-box 1.14.0 起"
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`
默认使用 `standard`
#### brutal_debug
启用 Hysteria Brutal CC 的调试信息日志记录。

View File

@@ -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 overridden by `outbound.domain_resolver`.
Can be overrides 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 overridden by `outbound.network_strategy`.
Can be overrides by `outbound.network_strategy`.
Conflicts with `default_interface`.

View File

@@ -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 override `dns.client_subnet`.
Will overrides `dns.client_subnet`.

View File

@@ -14,36 +14,6 @@ check [Migration](../migration/#migrate-inline-acme-to-certificate-provider).
Old fields will be removed in sing-box 1.16.0.
#### `strategy` in DNS rule actions
`strategy` in DNS rule actions is deprecated
and only supported in `legacyDNSMode`.
Old fields will be removed in sing-box 1.16.0.
#### `ip_accept_any` in DNS rules
`ip_accept_any` in DNS rules is deprecated
and only supported in `legacyDNSMode`.
Use `match_response` with response items instead.
Old fields will be removed in sing-box 1.16.0.
#### `rule_set_ip_cidr_accept_empty` in DNS rules
`rule_set_ip_cidr_accept_empty` in DNS rules is deprecated
and only supported in `legacyDNSMode`.
Old fields will be removed in sing-box 1.16.0.
#### Legacy address filter DNS rule items
Legacy address filter DNS rule items (`ip_cidr`, `ip_is_private` without `match_response`)
are deprecated and only supported in `legacyDNSMode`.
Use `match_response` with the `evaluate` action instead.
Old behavior will be removed in sing-box 1.16.0.
## 1.12.0
#### Legacy DNS server formats
@@ -51,7 +21,7 @@ Old behavior will be removed in sing-box 1.16.0.
DNS servers are refactored,
check [Migration](../migration/#migrate-to-new-dns-servers).
Old formats were removed in sing-box 1.14.0.
Compatibility for old formats will be removed in sing-box 1.14.0.
#### `outbound` DNS rule item

View File

@@ -14,36 +14,6 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则动作中的 `strategy`
DNS 规则动作中的 `strategy` 已废弃,
且仅在 `legacyDNSMode` 中可用。
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则中的 `ip_accept_any`
DNS 规则中的 `ip_accept_any` 已废弃,
且仅在 `legacyDNSMode` 中可用。
请使用 `match_response` 和响应项替代。
旧字段将在 sing-box 1.16.0 中被移除。
#### DNS 规则中的 `rule_set_ip_cidr_accept_empty`
DNS 规则中的 `rule_set_ip_cidr_accept_empty` 已废弃,
且仅在 `legacyDNSMode` 中可用。
旧字段将在 sing-box 1.16.0 中被移除。
#### 旧的地址筛选 DNS 规则项
旧的地址筛选 DNS 规则项(不使用 `match_response``ip_cidr``ip_is_private`)已废弃,
且仅在 `legacyDNSMode` 中可用。
请使用 `match_response``evaluate` 动作替代。
旧行为将在 sing-box 1.16.0 中被移除。
## 1.12.0
#### 旧的 DNS 服务器格式
@@ -51,7 +21,7 @@ DNS 规则中的 `rule_set_ip_cidr_accept_empty` 已废弃,
DNS 服务器已重构,
参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式).
旧格式在 sing-box 1.14.0 中被移除。
旧格式的兼容性将在 sing-box 1.14.0 中被移除。
#### `outbound` DNS 规则项

View File

@@ -57,6 +57,24 @@ 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",
@@ -93,45 +111,11 @@ 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: "`ip_accept_any` in DNS rules",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionRuleSetIPCIDRAcceptEmpty = Note{
Name: "dns-rule-rule-set-ip-cidr-accept-empty",
Description: "`rule_set_ip_cidr_accept_empty` in DNS rules",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionLegacyDNSAddressFilter = Note{
Name: "legacy-dns-address-filter",
Description: "legacy address filter DNS rule items",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule/",
}
var OptionLegacyDNSRuleStrategy = Note{
Name: "legacy-dns-rule-strategy",
Description: "`strategy` in DNS rule actions",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
MigrationLink: "https://sing-box.sagernet.org/configuration/dns/rule_action/",
}
var Options = []Note{
OptionLegacyDNSTransport,
OptionLegacyDNSFakeIPOptions,
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
OptionLegacyDomainStrategyOptions,
OptionInlineACME,
OptionIPAcceptAny,
OptionRuleSetIPCIDRAcceptEmpty,
OptionLegacyDNSAddressFilter,
OptionLegacyDNSRuleStrategy,
}

6
go.mod
View File

@@ -37,13 +37,13 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.3
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212
github.com/sagernet/sing-quic v0.6.0
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d
github.com/sagernet/sing-tun v0.8.7-0.20260327130747-85dc2d52a0b8
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7

12
go.sum
View File

@@ -236,20 +236,20 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.3 h1:zGMy9M1deBPEew9pCYIUHKeE+/lDQ5A2CBqjBjjzqkA=
github.com/sagernet/sing v0.8.3/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde h1:RNQzlpnsXIuu1HGts/fIzJ1PR7RhrzaNlU52MDyiX1c=
github.com/sagernet/sing v0.8.3-0.20260315153529-ed51f65fbfde/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212 h1:7mFOUqy+DyOj7qKGd1X54UMXbnbJiiMileK/tn17xYc=
github.com/sagernet/sing-quic v0.6.2-0.20260330152607-bf674c163212/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-quic v0.6.0 h1:dhrFnP45wgVKEOT1EvtsToxdzRnHIDIAgj6WHV9pLyM=
github.com/sagernet/sing-quic v0.6.0/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d h1:vi0j6301f6H8t2GYgAC2PA2AdnGdMwkP34B4+N03Qt4=
github.com/sagernet/sing-tun v0.8.7-0.20260323120017-8eb4e8acfc2d/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-tun v0.8.7-0.20260327130747-85dc2d52a0b8 h1:YxCs60xDya7R4NT+89v4Il+LjsSfCT/ceHegpe0xuls=
github.com/sagernet/sing-tun v0.8.7-0.20260327130747-85dc2d52a0b8/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=

View File

@@ -3,14 +3,19 @@ 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 {
@@ -21,29 +26,80 @@ type RawDNSOptions struct {
DNSClientOptions
}
type DNSOptions struct {
RawDNSOptions
type LegacyDNSOptions struct {
FakeIP *LegacyDNSFakeIPOptions `json:"fakeip,omitempty"`
}
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"
)
type DNSOptions struct {
RawDNSOptions
LegacyDNSOptions
}
type removedLegacyDNSOptions struct {
FakeIP json.RawMessage `json:"fakeip,omitempty"`
type contextKeyDontUpgrade struct{}
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
}
func (o *DNSOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error {
var legacyOptions removedLegacyDNSOptions
err := json.UnmarshalContext(ctx, content, &legacyOptions)
err := json.UnmarshalContext(ctx, content, &o.LegacyDNSOptions)
if err != nil {
return err
}
if len(legacyOptions.FakeIP) != 0 {
return E.New(legacyDNSFakeIPRemovedMessage)
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{}
}
return badjson.UnmarshallExcludedContext(ctx, content, legacyOptions, &o.RawDNSOptions)
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))
}
type DNSClientOptions struct {
@@ -55,6 +111,12 @@ 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)
}
@@ -67,6 +129,10 @@ 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)
}
@@ -82,7 +148,9 @@ func (o *DNSServerOptions) UnmarshalJSONContext(ctx context.Context, content []b
var options any
switch o.Type {
case "", C.DNSTypeLegacy:
return E.New(legacyDNSServerRemovedMessage)
o.Type = C.DNSTypeLegacy
options = new(LegacyDNSServerOptions)
deprecated.Report(ctx, deprecated.OptionLegacyDNSTransport)
default:
var loaded bool
options, loaded = registry.CreateOptions(o.Type)
@@ -95,6 +163,169 @@ 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
}
@@ -119,6 +350,16 @@ 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"`
@@ -126,6 +367,10 @@ type HostsDNSServerOptions struct {
type RawLocalDNSServerOptions struct {
DialerOptions
Legacy bool `json:"-"`
LegacyStrategy DomainStrategy `json:"-"`
LegacyDefaultDialer bool `json:"-"`
LegacyClientSubnet netip.Prefix `json:"-"`
}
type LocalDNSServerOptions struct {
@@ -136,6 +381,9 @@ type LocalDNSServerOptions struct {
type RemoteDNSServerOptions struct {
RawLocalDNSServerOptions
DNSServerAddressOptions
LegacyAddressResolver string `json:"-"`
LegacyAddressStrategy DomainStrategy `json:"-"`
LegacyAddressFallbackDelay badoption.Duration `json:"-"`
}
type RemoteTLSDNSServerOptions struct {

View File

@@ -2,7 +2,6 @@ package option
import (
"encoding/base64"
"strings"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
@@ -12,8 +11,6 @@ import (
"github.com/miekg/dns"
)
const defaultDNSRecordTTL uint32 = 3600
type DNSRCode int
func (r DNSRCode) MarshalJSON() ([]byte, error) {
@@ -79,13 +76,10 @@ func (o *DNSRecordOptions) UnmarshalJSON(data []byte) error {
if err == nil {
return o.unmarshalBase64(binary)
}
record, err := parseDNSRecord(stringValue)
record, err := dns.NewRR(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()
}
@@ -93,16 +87,6 @@ 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 {
@@ -116,10 +100,3 @@ 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)
}

View File

@@ -1,52 +0,0 @@
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 TestDNSRecordOptionsUnmarshalJSONAcceptsFullyQualifiedNames(t *testing.T) {
t.Parallel()
for _, record := range []string{
"example.com. A 1.1.1.1",
"www.example.com. IN CNAME example.com.",
} {
value := mustRecordOptions(t, record)
require.NotNil(t, value.RR)
}
}
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))
}

View File

@@ -1,72 +0,0 @@
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)
}
}
func TestDNSOptionsAcceptsTypedServers(t *testing.T) {
t.Parallel()
ctx := service.ContextWith[DNSTransportOptionsRegistry](context.Background(), stubDNSTransportOptionsRegistry{})
var options DNSOptions
err := json.UnmarshalContext(ctx, []byte(`{
"servers": [
{"type": "udp", "tag": "default", "server": "1.1.1.1"},
{"type": "fakeip", "tag": "fake", "inet4_range": "198.18.0.0/15"}
]
}`), &options)
require.NoError(t, err)
require.Len(t, options.Servers, 2)
require.Equal(t, C.DNSTypeUDP, options.Servers[0].Type)
require.Equal(t, "1.1.1.1", options.Servers[0].Options.(*RemoteDNSServerOptions).Server)
require.Equal(t, C.DNSTypeFakeIP, options.Servers[1].Type)
}

View File

@@ -19,7 +19,6 @@ type Hysteria2InboundOptions struct {
IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"`
InboundTLSOptionsContainer
Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"`
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
}
@@ -113,15 +112,13 @@ type Hysteria2MasqueradeString struct {
type Hysteria2OutboundOptions struct {
DialerOptions
ServerOptions
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
HopIntervalMax badoption.Duration `json:"hop_interval_max,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"`
HopInterval badoption.Duration `json:"hop_interval,omitempty"`
UpMbps int `json:"up_mbps,omitempty"`
DownMbps int `json:"down_mbps,omitempty"`
Obfs *Hysteria2Obfs `json:"obfs,omitempty"`
Password string `json:"password,omitempty"`
Network NetworkList `json:"network,omitempty"`
OutboundTLSOptionsContainer
BBRProfile string `json:"bbr_profile,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
BrutalDebug bool `json:"brutal_debug,omitempty"`
}

View File

@@ -1,7 +1,6 @@
package option
import (
"context"
"reflect"
C "github.com/sagernet/sing-box/constant"
@@ -34,24 +33,26 @@ func (r Rule) MarshalJSON() ([]byte, error) {
return badjson.MarshallObjects((_Rule)(r), v)
}
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)
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
return unmarshalDefaultRuleContext(ctx, payload, &r.DefaultOptions)
v = &r.DefaultOptions
case C.RuleTypeLogical:
return unmarshalLogicalRuleContext(ctx, payload, &r.LogicalOptions)
v = &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 {
@@ -159,64 +160,6 @@ 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)
}

View File

@@ -115,8 +115,6 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) {
case C.RuleActionTypeRoute:
r.Action = ""
v = r.RouteOptions
case C.RuleActionTypeEvaluate:
v = r.RouteOptions
case C.RuleActionTypeRouteOptions:
v = r.RouteOptionsOptions
case C.RuleActionTypeReject:
@@ -139,8 +137,6 @@ 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.RuleActionTypeRouteOptions:
v = &r.RouteOptionsOptions
case C.RuleActionTypeReject:

View File

@@ -35,7 +35,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.UnmarshalContext(ctx, bytes, (*_DNSRule)(r))
err := json.Unmarshal(bytes, (*_DNSRule)(r))
if err != nil {
return err
}
@@ -78,6 +78,12 @@ 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"`
@@ -104,23 +110,9 @@ 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"`
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"`
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,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"`
}
@@ -135,27 +127,11 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule)
if err != nil {
return err
}
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
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction)
}
func (r DefaultDNSRule) IsValid() bool {
@@ -180,27 +156,11 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) {
}
func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error {
rawAction, routeOptions, err := inspectDNSRuleAction(ctx, data)
err := json.Unmarshal(data, &r.RawLogicalDNSRule)
if err != nil {
return err
}
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
return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction)
}
func (r *LogicalDNSRule) IsValid() bool {

View File

@@ -1,133 +0,0 @@
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{}{}
}
}

View File

@@ -1,271 +0,0 @@
package option
import (
"context"
"testing"
C "github.com/sagernet/sing-box/constant"
"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 TestRuleRejectsNestedLogicalRuleAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "route",
"outbound": "direct",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedDefaultRuleZeroValueOutbound(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "outbound": ""}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedDefaultRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "udp_connect": false}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedLogicalRuleZeroValueAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleRejectsNestedLogicalRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"override_port": 0,
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, routeRuleActionNestedUnsupportedMessage)
}
func TestRuleAllowsTopLevelLogicalAction(t *testing.T) {
t.Parallel()
var rule Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"outbound": "direct",
"rules": [{"domain": "example.com"}]
}`), &rule)
require.NoError(t, err)
require.Equal(t, C.RuleActionTypeRoute, rule.LogicalOptions.Action)
require.Equal(t, "direct", rule.LogicalOptions.RouteOptions.Outbound)
}
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 TestDNSRuleRejectsNestedLogicalRuleAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "route",
"server": "default",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleZeroValueServer(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "server": ""}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedDefaultRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com", "disable_cache": false}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedLogicalRuleZeroValueAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"action": "",
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleRejectsNestedLogicalRuleZeroValueRouteOption(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{
"type": "logical",
"mode": "or",
"disable_cache": false,
"rules": [{"domain": "example.com"}]
}
]
}`), &rule)
require.ErrorContains(t, err, dnsRuleActionNestedUnsupportedMessage)
}
func TestDNSRuleAllowsTopLevelLogicalAction(t *testing.T) {
t.Parallel()
var rule DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"server": "default",
"rules": [{"domain": "example.com"}]
}`), &rule)
require.NoError(t, err)
require.Equal(t, C.RuleActionTypeRoute, rule.LogicalOptions.Action)
require.Equal(t, "default", rule.LogicalOptions.RouteOptions.Server)
}
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)
}

View File

@@ -125,7 +125,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
UDPTimeout: udpTimeout,
Handler: inbound,
MasqueradeHandler: masqueradeHandler,
BBRProfile: options.BBRProfile,
})
if err != nil {
return nil, err

View File

@@ -73,14 +73,12 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
ServerAddress: options.ServerOptions.Build(),
ServerPorts: options.ServerPorts,
HopInterval: time.Duration(options.HopInterval),
HopIntervalMax: time.Duration(options.HopIntervalMax),
SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps),
ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps),
SalamanderPassword: salamanderPassword,
Password: options.Password,
TLSConfig: tlsConfig,
UDPDisabled: !common.Contains(networkList, N.NetworkUDP),
BBRProfile: options.BBRProfile,
})
if err != nil {
return nil, err

View File

@@ -29,10 +29,7 @@ import (
"golang.org/x/net/http2/h2c"
)
var (
ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
WrapError func(error) error
)
var ConfigureHTTP3ListenerFunc func(ctx context.Context, logger logger.Logger, listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, options option.NaiveInboundOptions) (io.Closer, error)
func RegisterInbound(registry *inbound.Registry) {
inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound)

View File

@@ -179,18 +179,18 @@ type naiveConn struct {
func (c *naiveConn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.Conn, p)
return n, wrapError(err)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.Conn, p)
return n, wrapError(err)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
err := c.writeBufferWithPadding(c.Conn, buffer)
return wrapError(err)
return baderror.WrapH2(err)
}
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
@@ -210,7 +210,7 @@ type naiveH2Conn struct {
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.reader, p)
return n, wrapError(err)
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
@@ -218,7 +218,7 @@ func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
if err == nil {
c.flusher.Flush()
}
return n, wrapError(err)
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
@@ -227,15 +227,7 @@ func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
if err == nil {
c.flusher.Flush()
}
return wrapError(err)
}
func wrapError(err error) error {
err = baderror.WrapH2(err)
if WrapError != nil {
err = WrapError(err)
}
return err
return baderror.WrapH2(err)
}
func (c *naiveH2Conn) Close() error {

View File

@@ -124,5 +124,4 @@ func init() {
return quicListener, nil
}
naive.WrapError = qtls.WrapError
}

View File

@@ -614,7 +614,7 @@ func (t *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
return packetConn, nil
}
func (t *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
func (t *Endpoint) PrepareConnection(ctx context.Context, network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
tsFilter := t.filter.Load()
if tsFilter != nil {
var ipProto ipproto.Proto

View File

@@ -245,6 +245,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{
TunOptions: &inbound.tunOptions,
Context: ctx,
ConnContext: log.ContextWithNewID,
Handler: (*autoRedirectHandler)(inbound),
Logger: logger,
NetworkMonitor: networkManager.NetworkMonitor(),
@@ -257,7 +258,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if err != nil {
return nil, E.Cause(err, "initialize auto-redirect")
}
if !C.IsAndroid {
if C.IsLinux {
inbound.tunOptions.AutoRedirectMarkMode = true
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
if err != nil {
@@ -453,7 +454,7 @@ func (t *Inbound) Close() error {
)
}
func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
func (t *Inbound) PrepareConnection(ctx context.Context, network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
@@ -511,21 +512,35 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
type autoRedirectHandler Inbound
func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
func autoRedirectProcessInfoFromContext(ctx context.Context) *adapter.ConnectionOwner {
metadata := tun.AutoRedirectMetadataFromContext(ctx)
if metadata == nil {
return nil
}
return &adapter.ConnectionOwner{
ProcessID: metadata.ProcessID,
ProcessPath: metadata.ProcessPath,
UserId: metadata.UserId,
}
}
func (t *autoRedirectHandler) PrepareConnection(ctx context.Context, network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4
} else {
ipVersion = 6
}
routeDestination, err := t.router.PreMatch(adapter.InboundContext{
metadata := adapter.InboundContext{
Inbound: t.tag,
InboundType: C.TypeTun,
IPVersion: ipVersion,
Network: network,
Source: source,
Destination: destination,
}, routeContext, timeout, true)
ProcessInfo: autoRedirectProcessInfoFromContext(ctx),
}
routeDestination, err := t.router.PreMatch(metadata, routeContext, timeout, true)
if err != nil {
switch {
case rule.IsBypassed(err):
@@ -542,12 +557,12 @@ func (t *autoRedirectHandler) PrepareConnection(network string, source M.Socksad
}
func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
ctx = log.ContextWithNewID(ctx)
var metadata adapter.InboundContext
metadata.Inbound = t.tag
metadata.InboundType = C.TypeTun
metadata.Source = source
metadata.Destination = destination
metadata.ProcessInfo = autoRedirectProcessInfoFromContext(ctx)
t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source)
t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)

View File

@@ -129,7 +129,7 @@ func (w *Endpoint) Close() error {
return w.endpoint.Close()
}
func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
func (w *Endpoint) PrepareConnection(ctx context.Context, network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
var ipVersion uint8
if !destination.IsIPv6() {
ipVersion = 4

View File

@@ -393,6 +393,9 @@ func (r *NetworkManager) AutoRedirectOutputMark() uint32 {
}
func (r *NetworkManager) AutoRedirectOutputMarkFunc() control.Func {
if !C.IsLinux || C.IsAndroid {
return nil
}
return func(network, address string, conn syscall.RawConn) error {
if r.autoRedirectOutputMark == 0 {
return nil

View File

@@ -70,10 +70,6 @@ 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, "]")

View File

@@ -1,76 +0,0 @@
package rule
import (
"reflect"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
const (
routeRuleActionNestedUnsupportedMessage = "rule action is not supported in nested rules"
dnsRuleActionNestedUnsupportedMessage = "DNS rule action is not supported in nested rules"
)
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(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(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
}
}

View File

@@ -1,137 +0,0 @@
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/sagernet/sing/common/json"
"github.com/stretchr/testify/require"
)
func TestNewRulePreservesImplicitTopLevelDefaultAction(t *testing.T) {
t.Parallel()
var options option.Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"domain": "example.com"
}`), &options)
require.NoError(t, err)
rule, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), options, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewRuleAllowsNestedRuleWithoutAction(t *testing.T) {
t.Parallel()
var options option.Rule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com"}
]
}`), &options)
require.NoError(t, err)
rule, err := NewRule(context.Background(), log.NewNOPFactory().NewLogger("router"), options, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
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, routeRuleActionNestedUnsupportedMessage)
}
func TestNewDNSRulePreservesImplicitTopLevelDefaultAction(t *testing.T) {
t.Parallel()
var options option.DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"domain": "example.com"
}`), &options)
require.NoError(t, err)
rule, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), options, false, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
func TestNewDNSRuleAllowsNestedRuleWithoutAction(t *testing.T) {
t.Parallel()
var options option.DNSRule
err := json.UnmarshalContext(context.Background(), []byte(`{
"type": "logical",
"mode": "and",
"rules": [
{"domain": "example.com"}
]
}`), &options)
require.NoError(t, err)
rule, err := NewDNSRule(context.Background(), log.NewNOPFactory().NewLogger("dns"), options, false, false)
require.NoError(t, err)
require.NotNil(t, rule.Action())
require.Equal(t, C.RuleActionTypeRoute, rule.Action().Type())
}
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, dnsRuleActionNestedUnsupportedMessage)
}

View File

@@ -56,7 +56,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
func (r *abstractDefaultRule) destinationIPCIDRMatchesSource(metadata *adapter.InboundContext) bool {
return metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
return !metadata.IgnoreDestinationIPCIDRMatch && metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) destinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
@@ -156,6 +156,10 @@ 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)
}
return 0
}
return stateSet

View File

@@ -132,16 +132,6 @@ 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.RuleActionTypeRouteOptions:
return &RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
@@ -276,35 +266,18 @@ 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)
}
func formatDNSRouteAction(action string, server string, options RuleActionDNSRouteOptions) string {
var descriptions []string
descriptions = append(descriptions, server)
if options.DisableCache {
descriptions = append(descriptions, r.Server)
if r.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
if options.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL))
if r.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
}
if options.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", options.ClientSubnet))
if r.ClientSubnet.IsValid() {
descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet))
}
return F.ToString(action, "(", strings.Join(descriptions, ","), ")")
return F.ToString("route(", strings.Join(descriptions, ","), ")")
}
type RuleActionDNSRouteOptions struct {

View File

@@ -326,10 +326,6 @@ 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, "]")

View File

@@ -5,46 +5,37 @@ 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, legacyDNSMode bool) (adapter.DNSRule, error) {
func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer 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")
}
switch options.DefaultOptions.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
if options.DefaultOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions, legacyDNSMode)
return NewDefaultDNSRule(ctx, logger, options.DefaultOptions)
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")
}
switch options.LogicalOptions.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
case "", C.RuleActionTypeRoute:
if options.LogicalOptions.RouteOptions.Server == "" && checkServer {
return nil, E.New("missing server field")
}
}
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions, legacyDNSMode)
return NewLogicalDNSRule(ctx, logger, options.LogicalOptions)
default:
return nil, E.New("unknown rule type: ", options.Type)
}
@@ -54,20 +45,18 @@ 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, legacyDNSMode bool) (*DefaultDNSRule, error) {
func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*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)
@@ -127,7 +116,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 { //nolint:staticcheck
if len(options.Geosite) > 0 {
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 {
@@ -162,36 +151,11 @@ 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 { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionIPAcceptAny)
} else {
return nil, E.New("ip_accept_any is removed when legacyDNSMode is disabled, use ip_cidr with match_response")
}
if options.IPAcceptAny {
item := NewIPAcceptAnyItem()
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.ResponseRcode != nil {
item := NewDNSResponseRCodeItem(int(*options.ResponseRcode))
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseAnswer) > 0 {
item := NewDNSResponseRecordItem("response_answer", options.ResponseAnswer, dnsResponseAnswers)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseNs) > 0 {
item := NewDNSResponseRecordItem("response_ns", options.ResponseNs, dnsResponseNS)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.ResponseExtra) > 0 {
item := NewDNSResponseRecordItem("response_extra", options.ResponseExtra, dnsResponseExtra)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
@@ -320,14 +284,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
if options.RuleSetIPCIDRMatchSource {
matchSource = true
}
if options.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
if legacyDNSMode {
deprecated.Report(ctx, deprecated.OptionRuleSetIPCIDRAcceptEmpty)
} else {
return nil, E.New("rule_set_ip_cidr_accept_empty is removed when legacyDNSMode is disabled")
}
}
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty) //nolint:staticcheck
item := NewRuleSetItem(router, options.RuleSet, matchSource, options.RuleSetIPCIDRAcceptEmpty)
rule.ruleSetItem = item
rule.allItems = append(rule.allItems, item)
}
@@ -352,46 +309,15 @@ func (r *DefaultDNSRule) WithAddressLimit() bool {
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStatesForMatch(metadata).isEmpty()
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() {
metadata.IgnoreDestinationIPCIDRMatch = false
}()
return !r.matchStates(metadata).isEmpty()
}
func (r *DefaultDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
if r.matchResponse {
return !r.legacyMatchStatesForMatch(metadata).isEmpty()
}
return !r.abstractDefaultRule.legacyMatchStates(metadata).isEmpty()
}
func (r *DefaultDNSRule) matchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesForMatchWithMissingResponse(metadata, true)
}
func (r *DefaultDNSRule) legacyMatchStatesForMatch(metadata *adapter.InboundContext) ruleMatchStateSet {
return r.matchStatesForMatchWithMissingResponse(metadata, false)
}
func (r *DefaultDNSRule) matchStatesForMatchWithMissingResponse(metadata *adapter.InboundContext, ordinaryFailure bool) ruleMatchStateSet {
if r.matchResponse {
if metadata.DNSResponse == nil {
if ordinaryFailure {
return r.abstractDefaultRule.invertedFailure(0)
}
return 0
}
matchMetadata := *metadata
matchMetadata.IgnoreDestinationIPCIDRMatch = false
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.IgnoreDestinationIPCIDRMatch = false
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractDefaultRule.matchStates(&matchMetadata).isEmpty()
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
@@ -404,53 +330,7 @@ func (r *LogicalDNSRule) matchStates(metadata *adapter.InboundContext) ruleMatch
return r.abstractLogicalRule.matchStates(metadata)
}
func matchDNSHeadlessRuleStatesForMatch(rule adapter.HeadlessRule, metadata *adapter.InboundContext) ruleMatchStateSet {
switch rule := rule.(type) {
case *DefaultDNSRule:
return rule.matchStatesForMatch(metadata)
case *LogicalDNSRule:
return rule.matchStatesForMatch(metadata)
default:
return matchHeadlessRuleStates(rule, 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) {
func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.HeadlessRule, len(options.Rules)),
@@ -467,11 +347,7 @@ 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 {
err := validateNoNestedDNSRuleActions(subRule, true)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
rule, err := NewDNSRule(ctx, logger, subRule, false, legacyDNSMode)
rule, err := NewDNSRule(ctx, logger, subRule, false)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
@@ -501,17 +377,13 @@ func (r *LogicalDNSRule) WithAddressLimit() bool {
}
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
return !r.matchStatesForMatch(metadata).isEmpty()
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() {
metadata.IgnoreDestinationIPCIDRMatch = false
}()
return !r.matchStates(metadata).isEmpty()
}
func (r *LogicalDNSRule) LegacyPreMatch(metadata *adapter.InboundContext) bool {
return !r.abstractLogicalRule.legacyMatchStates(metadata).isEmpty()
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext, response *dns.Msg) bool {
matchMetadata := *metadata
matchMetadata.DNSResponse = response
matchMetadata.IgnoreDestinationIPCIDRMatch = false
matchMetadata.DestinationAddressMatchFromResponse = true
return !r.abstractLogicalRule.matchStates(&matchMetadata).isEmpty()
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return !r.matchStates(metadata).isEmpty()
}

View File

@@ -1,754 +0,0 @@
package rule
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"go4.org/netipx"
)
type legacyResponseLiteralKind uint8
const (
legacyLiteralRequireEmpty legacyResponseLiteralKind = iota
legacyLiteralRequireNonEmpty
legacyLiteralRequireSet
legacyLiteralForbidSet
)
type legacyResponseLiteral struct {
kind legacyResponseLiteralKind
ipSet *netipx.IPSet
}
type legacyResponseFormulaKind uint8
const (
legacyFormulaFalse legacyResponseFormulaKind = iota
legacyFormulaTrue
legacyFormulaLiteral
legacyFormulaAnd
legacyFormulaOr
)
type legacyResponseFormula struct {
kind legacyResponseFormulaKind
literal legacyResponseLiteral
children []legacyResponseFormula
}
type legacyResponseConstraint struct {
requireEmpty bool
requireNonEmpty bool
requiredSets []*netipx.IPSet
forbiddenSet *netipx.IPSet
}
const (
legacyRuleMatchDeferredDestinationAddress ruleMatchState = 1 << 4
legacyRuleMatchStateCount = 32
)
type legacyRuleMatchStateSet [legacyRuleMatchStateCount]legacyResponseFormula
var (
legacyAllIPSet = func() *netipx.IPSet {
var builder netipx.IPSetBuilder
builder.Complement()
return common.Must1(builder.IPSet())
}()
legacyNonPublicIPSet = func() *netipx.IPSet {
var builder netipx.IPSetBuilder
for _, prefix := range []string{
"0.0.0.0/32",
"10.0.0.0/8",
"127.0.0.0/8",
"169.254.0.0/16",
"172.16.0.0/12",
"192.168.0.0/16",
"224.0.0.0/4",
"::/128",
"::1/128",
"fc00::/7",
"fe80::/10",
"ff00::/8",
} {
builder.AddPrefix(netip.MustParsePrefix(prefix))
}
return common.Must1(builder.IPSet())
}()
)
func legacyFalseFormula() legacyResponseFormula {
return legacyResponseFormula{}
}
func legacyTrueFormula() legacyResponseFormula {
return legacyResponseFormula{kind: legacyFormulaTrue}
}
func legacyLiteralFormula(literal legacyResponseLiteral) legacyResponseFormula {
return legacyResponseFormula{
kind: legacyFormulaLiteral,
literal: literal,
}
}
func (f legacyResponseFormula) isFalse() bool {
return f.kind == legacyFormulaFalse
}
func (f legacyResponseFormula) isTrue() bool {
return f.kind == legacyFormulaTrue
}
func (f legacyResponseFormula) or(other legacyResponseFormula) legacyResponseFormula {
return legacyOrFormulas(f, other)
}
func (f legacyResponseFormula) and(other legacyResponseFormula) legacyResponseFormula {
return legacyAndFormulas(f, other)
}
func (f legacyResponseFormula) not() legacyResponseFormula {
switch f.kind {
case legacyFormulaFalse:
return legacyTrueFormula()
case legacyFormulaTrue:
return legacyFalseFormula()
case legacyFormulaLiteral:
return legacyLiteralFormula(legacyNegateResponseLiteral(f.literal))
case legacyFormulaAnd:
negated := make([]legacyResponseFormula, 0, len(f.children))
for _, child := range f.children {
negated = append(negated, child.not())
}
return legacyOrFormulas(negated...)
case legacyFormulaOr:
negated := make([]legacyResponseFormula, 0, len(f.children))
for _, child := range f.children {
negated = append(negated, child.not())
}
return legacyAndFormulas(negated...)
default:
panic("unknown legacy response formula kind")
}
}
func legacyNegateResponseLiteral(literal legacyResponseLiteral) legacyResponseLiteral {
switch literal.kind {
case legacyLiteralRequireEmpty:
return legacyResponseLiteral{kind: legacyLiteralRequireNonEmpty}
case legacyLiteralRequireNonEmpty:
return legacyResponseLiteral{kind: legacyLiteralRequireEmpty}
case legacyLiteralRequireSet:
return legacyResponseLiteral{kind: legacyLiteralForbidSet, ipSet: literal.ipSet}
case legacyLiteralForbidSet:
return legacyResponseLiteral{kind: legacyLiteralRequireSet, ipSet: literal.ipSet}
default:
panic("unknown legacy response literal kind")
}
}
func legacyOrFormulas(formulas ...legacyResponseFormula) legacyResponseFormula {
children := make([]legacyResponseFormula, 0, len(formulas))
for _, formula := range formulas {
if formula.isFalse() {
continue
}
if formula.isTrue() {
return legacyTrueFormula()
}
if formula.kind == legacyFormulaOr {
children = append(children, formula.children...)
continue
}
children = append(children, formula)
}
switch len(children) {
case 0:
return legacyFalseFormula()
case 1:
return children[0]
default:
return legacyResponseFormula{
kind: legacyFormulaOr,
children: children,
}
}
}
func legacyAndFormulas(formulas ...legacyResponseFormula) legacyResponseFormula {
children := make([]legacyResponseFormula, 0, len(formulas))
for _, formula := range formulas {
if formula.isFalse() {
return legacyFalseFormula()
}
if formula.isTrue() {
continue
}
if formula.kind == legacyFormulaAnd {
children = append(children, formula.children...)
continue
}
children = append(children, formula)
}
switch len(children) {
case 0:
return legacyTrueFormula()
case 1:
return children[0]
}
result := legacyResponseFormula{
kind: legacyFormulaAnd,
children: children,
}
if !result.satisfiable() {
return legacyFalseFormula()
}
return result
}
func (f legacyResponseFormula) satisfiable() bool {
return legacyResponseFormulasSatisfiable(legacyResponseConstraint{}, []legacyResponseFormula{f})
}
func legacyResponseFormulasSatisfiable(constraint legacyResponseConstraint, formulas []legacyResponseFormula) bool {
stack := append(make([]legacyResponseFormula, 0, len(formulas)), formulas...)
var disjunctions []legacyResponseFormula
for len(stack) > 0 {
formula := stack[len(stack)-1]
stack = stack[:len(stack)-1]
switch formula.kind {
case legacyFormulaFalse:
return false
case legacyFormulaTrue:
continue
case legacyFormulaLiteral:
var ok bool
constraint, ok = constraint.withLiteral(formula.literal)
if !ok {
return false
}
case legacyFormulaAnd:
stack = append(stack, formula.children...)
case legacyFormulaOr:
if len(formula.children) == 0 {
return false
}
disjunctions = append(disjunctions, formula)
default:
panic("unknown legacy response formula kind")
}
}
if len(disjunctions) == 0 {
return true
}
bestIndex := 0
for i := 1; i < len(disjunctions); i++ {
if len(disjunctions[i].children) < len(disjunctions[bestIndex].children) {
bestIndex = i
}
}
selected := disjunctions[bestIndex]
remaining := make([]legacyResponseFormula, 0, len(disjunctions)-1)
remaining = append(remaining, disjunctions[:bestIndex]...)
remaining = append(remaining, disjunctions[bestIndex+1:]...)
for _, child := range selected.children {
nextFormulas := make([]legacyResponseFormula, 0, len(remaining)+1)
nextFormulas = append(nextFormulas, remaining...)
nextFormulas = append(nextFormulas, child)
if legacyResponseFormulasSatisfiable(constraint, nextFormulas) {
return true
}
}
return false
}
func (c legacyResponseConstraint) withLiteral(literal legacyResponseLiteral) (legacyResponseConstraint, bool) {
switch literal.kind {
case legacyLiteralRequireEmpty:
c.requireEmpty = true
case legacyLiteralRequireNonEmpty:
c.requireNonEmpty = true
case legacyLiteralRequireSet:
requiredSets := make([]*netipx.IPSet, len(c.requiredSets)+1)
copy(requiredSets, c.requiredSets)
requiredSets[len(c.requiredSets)] = literal.ipSet
c.requiredSets = requiredSets
case legacyLiteralForbidSet:
c.forbiddenSet = legacyUnionIPSets(c.forbiddenSet, literal.ipSet)
default:
panic("unknown legacy response literal kind")
}
return c, c.satisfiable()
}
func (c legacyResponseConstraint) satisfiable() bool {
if c.requireEmpty && (c.requireNonEmpty || len(c.requiredSets) > 0) {
return false
}
if c.requireEmpty {
return true
}
for _, required := range c.requiredSets {
if !legacyIPSetHasAllowedIP(required, c.forbiddenSet) {
return false
}
}
if c.requireNonEmpty && len(c.requiredSets) == 0 {
return legacyIPSetHasAllowedIP(legacyAllIPSet, c.forbiddenSet)
}
return true
}
func legacyUnionIPSets(left *netipx.IPSet, right *netipx.IPSet) *netipx.IPSet {
if left == nil {
return right
}
if right == nil {
return left
}
var builder netipx.IPSetBuilder
builder.AddSet(left)
builder.AddSet(right)
return common.Must1(builder.IPSet())
}
func legacyIPSetHasAllowedIP(required *netipx.IPSet, forbidden *netipx.IPSet) bool {
if required == nil {
required = legacyAllIPSet
}
if forbidden == nil {
return len(required.Ranges()) > 0
}
builder := netipx.IPSetBuilder{}
builder.AddSet(required)
builder.RemoveSet(forbidden)
remaining := common.Must1(builder.IPSet())
return len(remaining.Ranges()) > 0
}
func legacySingleRuleMatchState(state ruleMatchState) legacyRuleMatchStateSet {
return legacySingleRuleMatchStateWithFormula(state, legacyTrueFormula())
}
func legacySingleRuleMatchStateWithFormula(state ruleMatchState, formula legacyResponseFormula) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
if !formula.isFalse() {
stateSet[state] = formula
}
return stateSet
}
func (s legacyRuleMatchStateSet) isEmpty() bool {
for _, formula := range s {
if !formula.isFalse() {
return false
}
}
return true
}
func (s legacyRuleMatchStateSet) merge(other legacyRuleMatchStateSet) legacyRuleMatchStateSet {
var merged legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
merged[state] = s[state].or(other[state])
}
return merged
}
func (s legacyRuleMatchStateSet) combine(other legacyRuleMatchStateSet) legacyRuleMatchStateSet {
if s.isEmpty() || other.isEmpty() {
return legacyRuleMatchStateSet{}
}
var combined legacyRuleMatchStateSet
for left := ruleMatchState(0); left < legacyRuleMatchStateCount; left++ {
if s[left].isFalse() {
continue
}
for right := ruleMatchState(0); right < legacyRuleMatchStateCount; right++ {
if other[right].isFalse() {
continue
}
combined[left|right] = combined[left|right].or(s[left].and(other[right]))
}
}
return combined
}
func (s legacyRuleMatchStateSet) withBase(base ruleMatchState) legacyRuleMatchStateSet {
if s.isEmpty() {
return legacyRuleMatchStateSet{}
}
var withBase legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
withBase[state|base] = withBase[state|base].or(s[state])
}
return withBase
}
func (s legacyRuleMatchStateSet) filter(allowed func(ruleMatchState) bool) legacyRuleMatchStateSet {
var filtered legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
if allowed(state) {
filtered[state] = s[state]
}
}
return filtered
}
func (s legacyRuleMatchStateSet) addBit(bit ruleMatchState) legacyRuleMatchStateSet {
var withBit legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
withBit[state|bit] = withBit[state|bit].or(s[state])
}
return withBit
}
func (s legacyRuleMatchStateSet) branchOnBit(bit ruleMatchState, condition legacyResponseFormula) legacyRuleMatchStateSet {
if condition.isFalse() {
return s
}
if condition.isTrue() {
return s.addBit(bit)
}
var branched legacyRuleMatchStateSet
conditionFalse := condition.not()
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
if state.has(bit) {
branched[state] = branched[state].or(s[state])
continue
}
branched[state] = branched[state].or(s[state].and(conditionFalse))
branched[state|bit] = branched[state|bit].or(s[state].and(condition))
}
return branched
}
func (s legacyRuleMatchStateSet) andFormula(formula legacyResponseFormula) legacyRuleMatchStateSet {
if formula.isFalse() || s.isEmpty() {
return legacyRuleMatchStateSet{}
}
if formula.isTrue() {
return s
}
var result legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if s[state].isFalse() {
continue
}
result[state] = s[state].and(formula)
}
return result
}
func (s legacyRuleMatchStateSet) anyFormula() legacyResponseFormula {
var formula legacyResponseFormula
for _, stateFormula := range s {
formula = formula.or(stateFormula)
}
return formula
}
type legacyRuleStateMatcher interface {
legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet
}
type legacyRuleStateMatcherWithBase interface {
legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet
}
func legacyMatchHeadlessRuleStates(rule adapter.HeadlessRule, metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return legacyMatchHeadlessRuleStatesWithBase(rule, metadata, 0)
}
func legacyMatchHeadlessRuleStatesWithBase(rule adapter.HeadlessRule, metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
if matcher, loaded := rule.(legacyRuleStateMatcherWithBase); loaded {
return matcher.legacyMatchStatesWithBase(metadata, base)
}
if matcher, loaded := rule.(legacyRuleStateMatcher); loaded {
return matcher.legacyMatchStates(metadata).withBase(base)
}
if rule.Match(metadata) {
return legacySingleRuleMatchState(base)
}
return legacyRuleMatchStateSet{}
}
func legacyMatchRuleItemStatesWithBase(item RuleItem, metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
if matcher, loaded := item.(legacyRuleStateMatcherWithBase); loaded {
return matcher.legacyMatchStatesWithBase(metadata, base)
}
if matcher, loaded := item.(legacyRuleStateMatcher); loaded {
return matcher.legacyMatchStates(metadata).withBase(base)
}
if item.Match(metadata) {
return legacySingleRuleMatchState(base)
}
return legacyRuleMatchStateSet{}
}
func (r *DefaultHeadlessRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.abstractDefaultRule.legacyMatchStates(metadata)
}
func (r *LogicalHeadlessRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.abstractLogicalRule.legacyMatchStates(metadata)
}
func (r *RuleSetItem) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *RuleSetItem) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, ruleSet := range r.setList {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
nestedMetadata.IPCIDRMatchSource = r.ipCidrMatchSource
nestedMetadata.IPCIDRAcceptEmpty = r.ipCidrAcceptEmpty
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(ruleSet, &nestedMetadata, base))
}
return stateSet
}
func (s *LocalRuleSet) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return s.legacyMatchStatesWithBase(metadata, 0)
}
func (s *LocalRuleSet) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
}
func (s *RemoteRuleSet) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return s.legacyMatchStatesWithBase(metadata, 0)
}
func (s *RemoteRuleSet) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
var stateSet legacyRuleMatchStateSet
for _, rule := range s.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleMatchCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, base))
}
return stateSet
}
func (r *abstractDefaultRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *abstractDefaultRule) legacyMatchStatesWithBase(metadata *adapter.InboundContext, inheritedBase ruleMatchState) legacyRuleMatchStateSet {
if len(r.allItems) == 0 {
return legacySingleRuleMatchState(inheritedBase)
}
evaluationBase := inheritedBase
if r.invert {
evaluationBase = 0
}
stateSet := legacySingleRuleMatchState(evaluationBase)
if len(r.sourceAddressItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourceAddressItems, metadata) {
stateSet = stateSet.addBit(ruleMatchSourceAddress)
}
}
if r.destinationIPCIDRMatchesSource(metadata) {
metadata.DidMatch = true
stateSet = stateSet.branchOnBit(ruleMatchSourceAddress, legacyDestinationIPFormula(r.destinationIPCIDRItems, metadata))
}
if len(r.sourcePortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.sourcePortItems, metadata) {
stateSet = stateSet.addBit(ruleMatchSourcePort)
}
}
if len(r.destinationAddressItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.destinationAddressItems, metadata) {
stateSet = stateSet.addBit(ruleMatchDestinationAddress)
}
}
if r.legacyDestinationIPCIDRMatchesDestination(metadata) {
metadata.DidMatch = true
stateSet = stateSet.branchOnBit(legacyRuleMatchDeferredDestinationAddress, legacyDestinationIPFormula(r.destinationIPCIDRItems, metadata))
}
if len(r.destinationPortItems) > 0 {
metadata.DidMatch = true
if matchAnyItem(r.destinationPortItems, metadata) {
stateSet = stateSet.addBit(ruleMatchDestinationPort)
}
}
for _, item := range r.items {
metadata.DidMatch = true
if !item.Match(metadata) {
if r.invert {
return legacySingleRuleMatchState(inheritedBase)
}
return legacyRuleMatchStateSet{}
}
}
if r.ruleSetItem != nil {
metadata.DidMatch = true
var merged legacyRuleMatchStateSet
for state := ruleMatchState(0); state < legacyRuleMatchStateCount; state++ {
if stateSet[state].isFalse() {
continue
}
nestedStateSet := legacyMatchRuleItemStatesWithBase(r.ruleSetItem, metadata, state)
merged = merged.merge(nestedStateSet.andFormula(stateSet[state]))
}
stateSet = merged
}
stateSet = stateSet.filter(func(state ruleMatchState) bool {
if r.legacyRequiresSourceAddressMatch(metadata) && !state.has(ruleMatchSourceAddress) {
return false
}
if len(r.sourcePortItems) > 0 && !state.has(ruleMatchSourcePort) {
return false
}
if r.legacyRequiresDestinationAddressMatch(metadata) && !state.has(ruleMatchDestinationAddress) {
return false
}
if r.legacyRequiresDeferredDestinationAddressMatch(metadata) && !state.has(legacyRuleMatchDeferredDestinationAddress) {
return false
}
if len(r.destinationPortItems) > 0 && !state.has(ruleMatchDestinationPort) {
return false
}
return true
})
if r.invert {
return legacySingleRuleMatchStateWithFormula(inheritedBase, stateSet.anyFormula().not())
}
return stateSet
}
func (r *abstractDefaultRule) legacyRequiresSourceAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.sourceAddressItems) > 0 || r.destinationIPCIDRMatchesSource(metadata)
}
func (r *abstractDefaultRule) legacyDestinationIPCIDRMatchesDestination(metadata *adapter.InboundContext) bool {
return !metadata.IPCIDRMatchSource && len(r.destinationIPCIDRItems) > 0
}
func (r *abstractDefaultRule) legacyRequiresDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return len(r.destinationAddressItems) > 0
}
func (r *abstractDefaultRule) legacyRequiresDeferredDestinationAddressMatch(metadata *adapter.InboundContext) bool {
return r.legacyDestinationIPCIDRMatchesDestination(metadata)
}
func (r *abstractLogicalRule) legacyMatchStates(metadata *adapter.InboundContext) legacyRuleMatchStateSet {
return r.legacyMatchStatesWithBase(metadata, 0)
}
func (r *abstractLogicalRule) legacyMatchStatesWithBase(metadata *adapter.InboundContext, base ruleMatchState) legacyRuleMatchStateSet {
evaluationBase := base
if r.invert {
evaluationBase = 0
}
var stateSet legacyRuleMatchStateSet
if r.mode == C.LogicalTypeAnd {
stateSet = legacySingleRuleMatchState(evaluationBase)
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.combine(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
if stateSet.isEmpty() && !r.invert {
return legacyRuleMatchStateSet{}
}
}
} else {
for _, rule := range r.rules {
nestedMetadata := *metadata
nestedMetadata.ResetRuleCache()
stateSet = stateSet.merge(legacyMatchHeadlessRuleStatesWithBase(rule, &nestedMetadata, evaluationBase))
}
}
if r.invert {
return legacySingleRuleMatchStateWithFormula(base, stateSet.anyFormula().not())
}
return stateSet
}
func legacyDestinationIPFormula(items []RuleItem, metadata *adapter.InboundContext) legacyResponseFormula {
if legacyDestinationIPResolved(metadata) {
if matchAnyItem(items, metadata) {
return legacyTrueFormula()
}
return legacyFalseFormula()
}
var formula legacyResponseFormula
for _, rawItem := range items {
switch item := rawItem.(type) {
case *IPCIDRItem:
if item.isSource || metadata.IPCIDRMatchSource {
if item.Match(metadata) {
return legacyTrueFormula()
}
continue
}
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireSet,
ipSet: item.ipSet,
}))
if metadata.IPCIDRAcceptEmpty {
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireEmpty,
}))
}
case *IPIsPrivateItem:
if item.isSource {
if item.Match(metadata) {
return legacyTrueFormula()
}
continue
}
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireSet,
ipSet: legacyNonPublicIPSet,
}))
case *IPAcceptAnyItem:
formula = formula.or(legacyLiteralFormula(legacyResponseLiteral{
kind: legacyLiteralRequireNonEmpty,
}))
default:
if rawItem.Match(metadata) {
return legacyTrueFormula()
}
}
}
return formula
}
func legacyDestinationIPResolved(metadata *adapter.InboundContext) bool {
return metadata.IPCIDRMatchSource ||
metadata.DestinationAddressMatchFromResponse ||
metadata.DNSResponse != nil ||
metadata.Destination.IsIP() ||
len(metadata.DestinationAddresses) > 0
}

View File

@@ -76,26 +76,11 @@ 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)
}
addresses := metadata.DestinationAddresses
if len(addresses) > 0 {
for _, address := range addresses {
if len(metadata.DestinationAddresses) > 0 {
for _, address := range metadata.DestinationAddresses {
if r.ipSet.Contains(address) {
return true
}

View File

@@ -13,9 +13,6 @@ func NewIPAcceptAnyItem() *IPAcceptAnyItem {
}
func (r *IPAcceptAnyItem) Match(metadata *adapter.InboundContext) bool {
if metadata.DestinationAddressMatchFromResponse {
return len(metadata.DNSResponseAddressesForMatch()) > 0
}
return len(metadata.DestinationAddresses) > 0
}

View File

@@ -1,6 +1,8 @@
package rule
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
N "github.com/sagernet/sing/common/network"
)
@@ -16,24 +18,21 @@ func NewIPIsPrivateItem(isSource bool) *IPIsPrivateItem {
}
func (r *IPIsPrivateItem) Match(metadata *adapter.InboundContext) bool {
var destination netip.Addr
if r.isSource {
return !N.IsPublicAddr(metadata.Source.Addr)
destination = metadata.Source.Addr
} else {
destination = metadata.Destination.Addr
}
if metadata.DestinationAddressMatchFromResponse {
for _, destinationAddress := range metadata.DNSResponseAddressesForMatch() {
if destination.IsValid() {
return !N.IsPublicAddr(destination)
}
if !r.isSource {
for _, destinationAddress := range metadata.DestinationAddresses {
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
}

View File

@@ -1,24 +0,0 @@
package rule
import (
"github.com/sagernet/sing-box/adapter"
F "github.com/sagernet/sing/common/format"
)
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=", r.rcode)
}

View File

@@ -1,63 +0,0 @@
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
}

View File

@@ -29,11 +29,9 @@ 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()
@@ -42,15 +40,6 @@ 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()
}

View File

@@ -1,139 +0,0 @@
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())
require.Empty(t, item.setList)
}
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())
}

View File

@@ -69,7 +69,3 @@ 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
}

View File

@@ -141,7 +141,6 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
metadata.ContainsProcessRule = HasHeadlessRule(headlessRules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = HasHeadlessRule(headlessRules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = HasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
metadata.ContainsDNSQueryTypeRule = HasHeadlessRule(headlessRules, isDNSQueryTypeHeadlessRule)
s.access.Lock()
s.rules = rules
s.metadata = metadata

View File

@@ -193,7 +193,6 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
s.metadata.ContainsProcessRule = HasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = HasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = HasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.metadata.ContainsDNSQueryTypeRule = HasHeadlessRule(plainRuleSet.Rules, isDNSQueryTypeHeadlessRule)
s.rules = rules
callbacks := s.callbacks.Array()
s.access.Unlock()

View File

@@ -2,7 +2,6 @@ package rule
import (
"context"
"net"
"net/netip"
"strings"
"testing"
@@ -15,7 +14,6 @@ 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"
)
@@ -583,7 +581,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, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
require.True(t, rule.MatchAddressLimit(&metadata))
})
t.Run("dns keeps ruleset or semantics", func(t *testing.T) {
t.Parallel()
@@ -598,7 +596,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, dnsResponseForTest(netip.MustParseAddr("203.0.113.1"))))
require.True(t, rule.MatchAddressLimit(&metadata))
})
t.Run("ruleset ip cidr flags stay scoped", func(t *testing.T) {
t.Parallel()
@@ -612,508 +610,10 @@ func TestDNSRuleSetSemantics(t *testing.T) {
ipCidrAcceptEmpty: true,
})
})
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.True(t, rule.MatchAddressLimit(&metadata))
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 TestDNSLegacyMatchResponseMissingResponseStillFailsClosed(t *testing.T) {
t.Parallel()
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
rule.invert = true
})
rule.matchResponse = true
metadata := testMetadata("lookup.example")
require.False(t, rule.LegacyPreMatch(&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("wrapper invert keeps nested deferred rule matchable", func(t *testing.T) {
t.Parallel()
nestedRule := dnsRuleForTest(func(rule *abstractDefaultRule) {
addDestinationIPIsPrivateItem(rule)
})
logicalRule := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
rules: []adapter.HeadlessRule{nestedRule},
mode: C.LogicalTypeAnd,
invert: true,
},
}
preLookupMetadata := testMetadata("lookup.example")
require.True(t, logicalRule.LegacyPreMatch(&preLookupMetadata))
matchedMetadata := testMetadata("lookup.example")
require.False(t, logicalRule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(netip.MustParseAddr("10.0.0.1"))))
unmatchedMetadata := testMetadata("lookup.example")
require.True(t, logicalRule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(netip.MustParseAddr("8.8.8.8"))))
})
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 TestDNSLegacyInvertNegationStressRegression(t *testing.T) {
t.Parallel()
const branchCount = 20
unmatchedResponse := dnsResponseForTest(netip.MustParseAddr("203.0.113.250"))
t.Run("logical wrapper", func(t *testing.T) {
t.Parallel()
branches := make([]adapter.HeadlessRule, 0, branchCount)
var matchedAddrs []netip.Addr
for i := 0; i < branchCount; i++ {
firstCIDR, secondCIDR, branchAddrs := legacyNegationBranchCIDRs(i)
if matchedAddrs == nil {
matchedAddrs = branchAddrs
}
branches = append(branches, &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
mode: C.LogicalTypeAnd,
rules: []adapter.HeadlessRule{
dnsRuleForTest(func(rule *abstractDefaultRule) {
addDestinationIPCIDRItem(t, rule, []string{firstCIDR})
}),
dnsRuleForTest(func(rule *abstractDefaultRule) {
addDestinationIPCIDRItem(t, rule, []string{secondCIDR})
}),
},
},
})
}
rule := &LogicalDNSRule{
abstractLogicalRule: abstractLogicalRule{
rules: branches,
mode: C.LogicalTypeOr,
invert: true,
},
}
preLookupMetadata := testMetadata("lookup.example")
require.True(t, rule.LegacyPreMatch(&preLookupMetadata))
matchedMetadata := testMetadata("lookup.example")
require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(matchedAddrs...)))
unmatchedMetadata := testMetadata("lookup.example")
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, unmatchedResponse))
})
t.Run("ruleset wrapper", func(t *testing.T) {
t.Parallel()
branches := make([]adapter.HeadlessRule, 0, branchCount)
var matchedAddrs []netip.Addr
for i := 0; i < branchCount; i++ {
firstCIDR, secondCIDR, branchAddrs := legacyNegationBranchCIDRs(i)
if matchedAddrs == nil {
matchedAddrs = branchAddrs
}
branches = append(branches, headlessLogicalRule(
C.LogicalTypeAnd,
false,
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationIPCIDRItem(t, rule, []string{firstCIDR})
}),
headlessDefaultRule(t, func(rule *abstractDefaultRule) {
addDestinationIPCIDRItem(t, rule, []string{secondCIDR})
}),
))
}
ruleSet := newLocalRuleSetForTest("dns-legacy-negation-stress", branches...)
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
rule.invert = true
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(matchedAddrs...)))
unmatchedMetadata := testMetadata("lookup.example")
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, unmatchedResponse))
})
}
func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
@@ -1165,14 +665,14 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
matchedMetadata := testMetadata("lookup.example")
matchedMetadata.DestinationAddresses = testCase.matchedAddrs
require.False(t, rule.MatchAddressLimit(&matchedMetadata, dnsResponseForTest(testCase.matchedAddrs...)))
require.False(t, rule.MatchAddressLimit(&matchedMetadata))
unmatchedMetadata := testMetadata("lookup.example")
unmatchedMetadata.DestinationAddresses = testCase.unmatchedAddrs
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata, dnsResponseForTest(testCase.unmatchedAddrs...)))
require.True(t, rule.MatchAddressLimit(&unmatchedMetadata))
})
}
t.Run("mixed resolved and deferred fields invert matches pre lookup", func(t *testing.T) {
t.Run("mixed resolved and deferred fields keep old pre lookup false", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("lookup.example")
rule := dnsRuleForTest(func(rule *abstractDefaultRule) {
@@ -1180,9 +680,9 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
addOtherItem(rule, NewNetworkItem([]string{N.NetworkTCP}))
addDestinationIPCIDRItem(t, rule, []string{"203.0.113.0/24"})
})
require.True(t, rule.Match(&metadata))
require.False(t, rule.Match(&metadata))
})
t.Run("ruleset only deferred fields invert matches pre lookup", func(t *testing.T) {
t.Run("ruleset only deferred fields keep old pre lookup false", func(t *testing.T) {
t.Parallel()
metadata := testMetadata("lookup.example")
ruleSet := newLocalRuleSetForTest("dns-ruleset-ipcidr", headlessDefaultRule(t, func(rule *abstractDefaultRule) {
@@ -1192,7 +692,7 @@ func TestDNSInvertAddressLimitPreLookupRegression(t *testing.T) {
rule.invert = true
addRuleSetItem(rule, &RuleSetItem{setList: []adapter.RuleSet{ruleSet}})
})
require.True(t, rule.Match(&metadata))
require.False(t, rule.Match(&metadata))
})
}
@@ -1263,45 +763,6 @@ 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 legacyNegationBranchCIDRs(index int) (string, string, []netip.Addr) {
first := netip.AddrFrom4([4]byte{198, 18, 0, byte(index*2 + 1)})
second := netip.AddrFrom4([4]byte{198, 18, 0, byte(index*2 + 2)})
return first.String() + "/32", second.String() + "/32", []netip.Addr{first, second}
}
func addRuleSetItem(rule *abstractDefaultRule, item *RuleSetItem) {
rule.ruleSetItem = item
rule.allItems = append(rule.allItems, item)