Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
034727c
Add evaluate DNS rule action and related rule items
nekohasekai Mar 24, 2026
33e4fcc
Reorder DNS rule item fields: match_response above address filter and…
nekohasekai Mar 24, 2026
27b6005
Fix DNS evaluate routing regressions
nekohasekai Mar 24, 2026
2380ae8
Fix DNS record parsing and matching regressions
nekohasekai Mar 24, 2026
75ddbee
Fix DNS match_response response address handling
nekohasekai Mar 24, 2026
23efd0c
dns: use response-only address matching
nekohasekai Mar 24, 2026
04dd096
Fix DNS evaluate regressions
nekohasekai Mar 24, 2026
58334cf
Fix DNS pre-match CIDR fail-closed semantics
nekohasekai Mar 24, 2026
8b75fbf
dns: document non-response rule_set address-filter semantics
nekohasekai Mar 25, 2026
8096507
Remove legacy DNS server formats
nekohasekai Mar 25, 2026
a0d9c27
dns: preserve legacy address-filter pre-match semantics
nekohasekai Mar 25, 2026
abd420b
dns: isolate legacy pre-match semantics
nekohasekai Mar 25, 2026
5604488
Fix legacy DNS negation expansion
nekohasekai Mar 25, 2026
259e67f
dns: complete lookup rule execution in new mode
nekohasekai Mar 25, 2026
40b9c64
dns: make rule path selection rule-set aware
nekohasekai Mar 26, 2026
e09a6d3
dns: restore init validation and fix rule-set query type
nekohasekai Mar 26, 2026
a491c9a
Fix DNS record parsing and shutdown race
nekohasekai Mar 26, 2026
5cc484f
Fix DNS record parser file inclusion and rule match log index
nekohasekai Mar 26, 2026
5dbabe4
dns: restore lookup reject semantics
nekohasekai Mar 27, 2026
ae2c869
Fix legacy DNS rule_set accept_empty matching
nekohasekai Mar 27, 2026
876c8eb
Fix DNS rule-set ref handling
nekohasekai Mar 27, 2026
036ef04
Make DNS match_response fail as a normal condition
nekohasekai Mar 28, 2026
4df38c3
dns: make rule strategy legacy-only
nekohasekai Mar 28, 2026
f02b507
option: reject nested rule actions
nekohasekai Mar 28, 2026
ccad6d4
Use typed SVCB hint structs instead of string parsing
nekohasekai Mar 29, 2026
0a85afe
docs: add evaluate action, response matching fields, and deprecation …
nekohasekai Mar 29, 2026
805f073
Suppress SA1019 lint warnings for intentional deprecated field usage
nekohasekai Mar 29, 2026
b44cf24
docs: fix grammar errors and typos
nekohasekai Mar 31, 2026
bd222fe
dns: serialize rebuilds and keep last good rules on failure
nekohasekai Mar 31, 2026
8667313
dns: use refcounted snapshot to narrow rule lock scope
nekohasekai Mar 31, 2026
be4e696
route/rule: remove dead IgnoreDestinationIPCIDRMatch field
nekohasekai Mar 31, 2026
19b2e48
dns: populate reverse mapping for legacy predefined responses
nekohasekai Mar 31, 2026
1897e51
docs: fix strategy deprecation format, explain legacyDNSMode, unify C…
nekohasekai Mar 31, 2026
e6377f7
fix: add missing EnvName, document Strategy invariant, improve rcode …
nekohasekai Mar 31, 2026
6a4b0db
dns: fix test style issues in repro_test.go
nekohasekai Mar 31, 2026
a83f2e9
dns: improve test coverage and cleanup
nekohasekai Mar 31, 2026
ef99a87
dns: reject method `reply` is not supported for DNS rules
nekohasekai Mar 31, 2026
509da1c
dns: return immediately on context cancellation in evaluate exchange
nekohasekai Mar 31, 2026
47b3ca1
dns: fix err shadowing in buildRules
nekohasekai Mar 31, 2026
faf786c
dns: fix variable shadowing in matchDNSHeadlessRuleStatesForMatch
nekohasekai Mar 31, 2026
d3a5e47
adapter: remove unused DestinationAddressesForMatch
nekohasekai Mar 31, 2026
1579aee
dns: remove dead lookup strategy guard in lookupWithRulesType
nekohasekai Mar 31, 2026
3b1ed3c
dns: remove dead lookupStrategyAllowsQueryType helper
nekohasekai Mar 31, 2026
8860214
dns: remove redundant queryOptions variable
nekohasekai Mar 31, 2026
35ac4dc
dns: remove redundant DNSResponse assignment in addressLimitResponseC…
nekohasekai Mar 31, 2026
8502f14
dns: add evaluate integration tests for response_rcode, response_ns, …
nekohasekai Mar 31, 2026
58dfd2e
option: add round-trip test for DNSRuleAction with evaluate action
nekohasekai Mar 31, 2026
c1ff6a0
Format code
nekohasekai Apr 1, 2026
c17a5a3
Simplify nested action validation and fix FallbackNetworkType bug
nekohasekai Apr 1, 2026
7757a78
Fix minor robustness issues found during code review
nekohasekai Apr 1, 2026
8916a24
test: remove low-value DNS WHAT tests
nekohasekai Apr 1, 2026
99b363c
test: remove internal-state assertions that test through unexported f…
nekohasekai Apr 1, 2026
91f942c
Simplify DNS router internals
nekohasekai Apr 1, 2026
7834854
Replace internal terminology in docs and error messages
nekohasekai Apr 1, 2026
40e40ea
Fix evaluate response-match validation
nekohasekai Apr 1, 2026
bbccdbc
dns: reject evaluate fakeip servers
nekohasekai Apr 1, 2026
6daed34
Add DNS respond rule action
nekohasekai Apr 1, 2026
3c7bc5a
Unify evaluate-produced DNS message terminology to "evaluated response"
nekohasekai Apr 1, 2026
8e10b22
Standardize legacy DNS feature terminology in docs and error messages
nekohasekai Apr 1, 2026
c3fdf52
dns: validate rule-set updates before commit
nekohasekai Apr 1, 2026
81430c4
docs: fix broken anchors, change block ordering, and fakeip field name
nekohasekai Apr 1, 2026
0926405
dns: hard-fail lookup split rule misuse
nekohasekai Apr 1, 2026
250bddf
dns: allow rule-set updates that keep new mode
nekohasekai Apr 1, 2026
dfa4603
dns: ignore split lookup errors on partial success
nekohasekai Apr 2, 2026
7468875
dns: simplify evaluate action transport resolution
nekohasekai Apr 2, 2026
646ed69
dns: unify match_response gate error for all Response Match Fields
nekohasekai Apr 2, 2026
a1a0d83
dns: revert legacy pre-match to simple flag-based approach
nekohasekai Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 2 additions & 7 deletions adapter/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ type DNSRouter interface {

type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
ClearCache()
}

Expand Down Expand Up @@ -72,11 +72,6 @@ type DNSTransport interface {
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}

type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}

type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
Expand Down
65 changes: 57 additions & 8 deletions adapter/inbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"

"github.com/miekg/dns"
)

type Inbound interface {
Expand Down Expand Up @@ -79,14 +81,16 @@ type InboundContext struct {
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration

DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
DestinationAddresses []netip.Addr
DNSResponse *dns.Msg
DestinationAddressMatchFromResponse bool
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool

// rule cache

Expand Down Expand Up @@ -115,6 +119,51 @@ func (c *InboundContext) ResetRuleMatchCache() {
c.DidMatch = false
}

func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
return DNSResponseAddresses(c.DNSResponse)
}

func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawRecord := range response.Answer {
switch record := rawRecord.(type) {
case *dns.A:
addr := M.AddrFromIP(record.A)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.AAAA:
addr := M.AddrFromIP(record.AAAA)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.HTTPS:
for _, value := range record.SVCB.Value {
switch hint := value.(type) {
case *dns.SVCBIPv4Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip).Unmap()
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
case *dns.SVCBIPv6Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip)
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
}
}
}
}
return addresses
}

type inboundContextKey struct{}

func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
Expand Down
45 changes: 45 additions & 0 deletions adapter/inbound_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package adapter

import (
"net"
"net/netip"
"testing"

"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)

func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
t.Parallel()

ipv4Hint := net.ParseIP("1.1.1.1")
require.NotNil(t, ipv4Hint)

response := &dns.Msg{
MsgHdr: dns.MsgHdr{
Response: true,
Rcode: dns.RcodeSuccess,
},
Answer: []dns.RR{
&dns.HTTPS{
SVCB: dns.SVCB{
Hdr: dns.RR_Header{
Name: dns.Fqdn("example.com"),
Rrtype: dns.TypeHTTPS,
Class: dns.ClassINET,
Ttl: 60,
},
Priority: 1,
Target: ".",
Value: []dns.SVCBKeyValue{
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
},
},
},
},
}

addresses := DNSResponseAddresses(response)
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
require.True(t, addresses[0].Is4())
}
12 changes: 9 additions & 3 deletions adapter/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,16 @@ type RuleSet interface {

type RuleSetUpdateCallback func(it RuleSet)

type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
}

// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
}
type HTTPStartContext struct {
ctx context.Context
Expand Down
7 changes: 5 additions & 2 deletions adapter/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package adapter

import (
C "github.com/sagernet/sing-box/constant"

"github.com/miekg/dns"
)

type HeadlessRule interface {
Expand All @@ -18,8 +20,9 @@ type Rule interface {

type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
}

type RuleAction interface {
Expand All @@ -29,7 +32,7 @@ type RuleAction interface {

func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
return false
default:
return true
Expand Down
3 changes: 2 additions & 1 deletion box.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
Expand Down Expand Up @@ -486,7 +487,7 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
if err != nil {
return err
}
Expand Down
25 changes: 12 additions & 13 deletions constant/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@ const (
)

const (
DNSTypeLegacy = "legacy"
DNSTypeLegacyRcode = "legacy_rcode"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local"
DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
DNSTypeLegacy = "legacy"
DNSTypeUDP = "udp"
DNSTypeTCP = "tcp"
DNSTypeTLS = "tls"
DNSTypeHTTPS = "https"
DNSTypeQUIC = "quic"
DNSTypeHTTP3 = "h3"
DNSTypeLocal = "local"
DNSTypeHosts = "hosts"
DNSTypeFakeIP = "fakeip"
DNSTypeDHCP = "dhcp"
DNSTypeTailscale = "tailscale"
)

const (
Expand Down
2 changes: 2 additions & 0 deletions constant/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const (
const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeEvaluate = "evaluate"
RuleActionTypeRespond = "respond"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"
Expand Down
33 changes: 5 additions & 28 deletions dns/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"

"github.com/sagernet/sing-box/adapter"
Expand All @@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
Expand Down Expand Up @@ -109,7 +107,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}

func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
Expand Down Expand Up @@ -239,13 +237,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else {
rejected = !responseChecker(MessageToAddresses(response))
rejected = !responseChecker(response)
}
if rejected {
if !disableCache && c.rdrc != nil {
Expand Down Expand Up @@ -315,7 +310,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, nil
}

func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
var strategy C.DomainStrategy
Expand Down Expand Up @@ -400,7 +395,7 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
}
}

func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := dns.Question{
Name: name,
Qtype: qType,
Expand Down Expand Up @@ -515,25 +510,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
}

func MessageToAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(answer.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
case *dns.HTTPS:
for _, value := range answer.SVCB.Value {
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
}
}
}
}
return addresses
return adapter.DNSResponseAddresses(response)
}

func wrapError(err error) error {
Expand Down
Loading
Loading