Compare commits

..

34 Commits

Author SHA1 Message Date
dependabot[bot]
fce764a6f0 build(deps): bump github.com/go-jose/go-jose/v4 from 4.1.3 to 4.1.4
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 04:13:38 +00:00
世界
ebd31ca363 Add optimistic DNS cache 2026-04-11 12:10:52 +08:00
世界
6ba7a6f001 Fix tailscale error 2026-04-11 11:48:54 +08:00
世界
b7e1a14974 Fix darwin cgo DNS again 2026-04-10 23:17:56 +08:00
世界
a5c0112f0c Update naiveproxy to v147.0.7727.49-1 2026-04-10 20:59:59 +08:00
世界
e6427e8244 Bump version 2026-04-10 16:24:31 +08:00
世界
c0d9551bcf documentation: Fix missing update for ip_version and query_type 2026-04-10 16:24:31 +08:00
世界
5cdf1aa000 Add cloudflared inbound 2026-04-10 16:24:31 +08:00
世界
6da0aa0c82 Fix lint errors 2026-04-10 16:24:30 +08:00
世界
97f4723467 platform: Wrap command RPC error returns with E.Cause 2026-04-10 16:24:30 +08:00
世界
6c7fb1dad1 Add package_name_regex route, DNS and headless rule item 2026-04-10 16:24:30 +08:00
世界
e0696f5e94 documentation: Fixes 2026-04-10 16:24:30 +08:00
世界
ddcaf040e2 Un-deprecate ip_accept_any DNS rule item 2026-04-10 16:24:29 +08:00
世界
57039ac11d tools: Tailscale status 2026-04-10 16:24:29 +08:00
世界
abd6baf3cb Fix darwin local DNS transport 2026-04-10 16:24:29 +08:00
世界
a48fd106c3 Fix rules lock 2026-04-10 16:24:29 +08:00
世界
6dfab9225f Revert "Also enable certificate store by default on Apple platforms"
This reverts commit 62cb06c02f.
2026-04-10 16:24:28 +08:00
世界
5e7e58f5e9 tools: Tailscale status 2026-04-10 16:24:28 +08:00
世界
cfcc766d74 platform: Fix darwin signal handler 2026-04-10 16:24:28 +08:00
世界
a24170638e tools: Network Quality & STUN 2026-04-10 16:24:27 +08:00
世界
ac9c0e7a81 oom-killer: Free memory on pressure notification and use gradual interval backoff 2026-04-10 16:24:27 +08:00
世界
51166f4601 Fix deprecated warning double-formatting on localized clients 2026-04-10 16:24:27 +08:00
世界
5d254d9015 platform: Fix set local 2026-04-10 16:24:26 +08:00
nekohasekai
d3fc58ceb8 Add evaluate DNS rule action and related rule items 2026-04-10 16:24:26 +08:00
世界
58d22df1be Also enable certificate store by default on Apple platforms
`SecTrustEvaluateWithError` is serial
2026-04-10 16:24:26 +08:00
世界
574852bdc1 platform: Add OOM Report & Crash Rerport 2026-04-10 16:24:25 +08:00
世界
ddc181f65a Add BBR profile and hop interval randomization for Hysteria2 2026-04-10 16:24:25 +08:00
nekohasekai
e2727d9556 Refactor ACME support to certificate provider 2026-04-10 16:24:25 +08:00
世界
f8b05790d1 cronet-go: Update chromium to 145.0.7632.159 2026-04-10 16:24:25 +08:00
世界
c1203821f9 documentation: Update descriptions for neighbor rules 2026-04-10 16:24:24 +08:00
世界
9805db343c Add macOS support for MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
b28083b131 Add Android support for MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
0d1ce7957d Add MAC and hostname rule items 2026-04-10 16:24:24 +08:00
世界
025b947a24 Bump version 2026-04-10 16:23:45 +08:00
42 changed files with 1779 additions and 516 deletions

View File

@@ -1 +1 @@
ea7cd33752aed62603775af3df946c1b83f4b0b3
335e5bef5d88fc4474c9a70b865561f45a67de83

View File

@@ -3,6 +3,7 @@ package adapter
import (
"context"
"net/netip"
"time"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@@ -31,12 +32,13 @@ type DNSClient interface {
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
@@ -49,11 +51,12 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
DisableOptimisticCache: options.DisableOptimisticCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
@@ -63,6 +66,13 @@ type RDRCStore interface {
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
}
type DNSCacheStore interface {
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
ClearDNSCache() error
}
type DNSTransport interface {
Lifecycle
Type() string

View File

@@ -47,6 +47,12 @@ type CacheFile interface {
StoreRDRC() bool
RDRCStore
StoreDNS() bool
DNSCacheStore
SetDisableExpire(disableExpire bool)
SetOptimisticTimeout(timeout time.Duration)
LoadMode() string
StoreMode(mode string) error
LoadSelected(group string) string

7
box.go
View File

@@ -196,7 +196,10 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize DNS router")
}
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
@@ -372,7 +375,7 @@ func New(options Options) (*Box, error) {
}
}
if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
internalServices = append(internalServices, cacheFile)
}

View File

@@ -87,11 +87,12 @@ func NewWithOptions(options Options) (N.Dialer, error) {
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
Transport: transport,
Strategy: strategy,
DisableCache: dialOptions.DomainResolver.DisableCache,
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
}
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else if options.DirectResolver {

View File

@@ -9,6 +9,8 @@ import (
"net/netip"
"time"
"github.com/sagernet/sing/common/bufio"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -431,6 +433,9 @@ func Run(options Options) (*Result, error) {
defer func() {
_ = packetConn.Close()
}()
if deadline.NeedAdditionalReadDeadline(packetConn) {
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
}
select {
case <-ctx.Done():

View File

@@ -30,59 +30,63 @@ var (
var _ adapter.DNSClient = (*Client)(nil)
type Client struct {
timeout time.Duration
disableCache bool
disableExpire bool
independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger
cache freelru.Cache[dns.Question, *dns.Msg]
cacheLock compatible.Map[dns.Question, chan struct{}]
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
transportCacheLock compatible.Map[dns.Question, chan struct{}]
ctx context.Context
timeout time.Duration
disableCache bool
disableExpire bool
optimisticTimeout time.Duration
cacheCapacity uint32
clientSubnet netip.Prefix
rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore
dnsCache adapter.DNSCacheStore
initDNSCacheFunc func() adapter.DNSCacheStore
logger logger.ContextLogger
cache freelru.Cache[dnsCacheKey, *dns.Msg]
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
}
type ClientOptions struct {
Timeout time.Duration
DisableCache bool
DisableExpire bool
IndependentCache bool
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
Logger logger.ContextLogger
Context context.Context
Timeout time.Duration
DisableCache bool
DisableExpire bool
OptimisticTimeout time.Duration
CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore
DNSCache func() adapter.DNSCacheStore
Logger logger.ContextLogger
}
func NewClient(options ClientOptions) *Client {
client := &Client{
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
cacheCapacity := options.CacheCapacity
if cacheCapacity < 1024 {
cacheCapacity = 1024
}
if !client.disableCache {
if !client.independentCache {
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
} else {
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
}
client := &Client{
ctx: options.Context,
timeout: options.Timeout,
disableCache: options.DisableCache,
disableExpire: options.DisableExpire,
optimisticTimeout: options.OptimisticTimeout,
cacheCapacity: cacheCapacity,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC,
initDNSCacheFunc: options.DNSCache,
logger: options.Logger,
}
if client.timeout == 0 {
client.timeout = C.DNSTimeout
}
if !client.disableCache && client.initDNSCacheFunc == nil {
client.initializeMemoryCache()
}
return client
}
type transportCacheKey struct {
type dnsCacheKey struct {
dns.Question
transportTag string
}
@@ -91,6 +95,19 @@ func (c *Client) Start() {
if c.initRDRCFunc != nil {
c.rdrc = c.initRDRCFunc()
}
if c.initDNSCacheFunc != nil {
c.dnsCache = c.initDNSCacheFunc()
}
if c.dnsCache == nil {
c.initializeMemoryCache()
}
}
func (c *Client) initializeMemoryCache() {
if c.disableCache || c.cache != nil {
return
}
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
}
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
@@ -107,6 +124,37 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}
func computeTimeToLive(response *dns.Msg) uint32 {
var timeToLive uint32
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
return soaTTL
}
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
return timeToLive
}
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
}
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 {
@@ -121,13 +169,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
return FixedResponseStatus(message, dns.RcodeSuccess), nil
}
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
message = c.prepareExchangeMessage(message, options)
isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 &&
@@ -139,40 +181,32 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
!options.ClientSubnet.IsValid()
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
if !disableCache {
if c.cache != nil {
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(question)
close(cond)
}()
}
} else if c.transportCache != nil {
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.transportCacheLock.Delete(question)
close(cond)
}()
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
if loaded {
select {
case <-cond:
case <-ctx.Done():
return nil, ctx.Err()
}
} else {
defer func() {
c.cacheLock.Delete(cacheKey)
close(cond)
}()
}
response, ttl := c.loadResponse(question, transport)
response, ttl, isStale := c.loadResponse(question, transport)
if response != nil {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
if isStale && !options.DisableOptimisticCache {
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
response.Id = message.Id
return response, nil
} else if !isStale {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
return response, nil
}
}
}
@@ -188,52 +222,10 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return nil, ErrResponseRejectedCached
}
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
response, err := transport.Exchange(ctx, message)
cancel()
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
response = FixedResponseStatus(message, int(rcodeError))
} else {
return nil, err
}
return nil, err
}
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
validResponse := response
loop:
for {
var (
addresses int
queryCNAME string
)
for _, rawRR := range validResponse.Answer {
switch rr := rawRR.(type) {
case *dns.A:
break loop
case *dns.AAAA:
break loop
case *dns.CNAME:
queryCNAME = rr.Target
}
}
if queryCNAME == "" {
break
}
exMessage := *message
exMessage.Question = []dns.Question{{
Name: queryCNAME,
Qtype: question.Qtype,
}}
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
if err != nil {
return nil, err
}
}
if validResponse != response {
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
@@ -250,54 +242,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, ErrResponseRejected
}
}
if question.Qtype == dns.TypeHTTPS {
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
} else {
return it.Key() != dns.SVCB_IPV4HINT
}
})
https.SVCB = content
}
}
}
var timeToLive uint32
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
timeToLive = soaTTL
}
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
}
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = timeToLive
}
}
timeToLive := applyResponseOptions(question, response, options)
if !disableCache {
c.storeCache(transport, question, response, timeToLive)
}
@@ -363,8 +308,12 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
func (c *Client) ClearCache() {
if c.cache != nil {
c.cache.Purge()
} else if c.transportCache != nil {
c.transportCache.Purge()
}
if c.dnsCache != nil {
err := c.dnsCache.ClearDNSCache()
if err != nil && c.logger != nil {
c.logger.Warn("clear DNS cache: ", err)
}
}
}
@@ -380,24 +329,22 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
if timeToLive == 0 {
return
}
if c.dnsCache != nil {
packed, err := message.Pack()
if err == nil {
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
}
return
}
if c.cache == nil {
return
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.disableExpire {
if !c.independentCache {
c.cache.Add(question, message.Copy())
} else {
c.transportCache.Add(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy())
}
c.cache.Add(key, message.Copy())
} else {
if !c.independentCache {
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
} else {
c.transportCache.AddWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
}, message.Copy(), time.Second*time.Duration(timeToLive))
}
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
}
}
@@ -407,19 +354,19 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
Qtype: qType,
Qclass: dns.ClassINET,
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(question, transport)
if err != ErrNotCached {
return cachedAddresses, err
}
}
message := dns.Msg{
MsgHdr: dns.MsgHdr{
RecursionDesired: true,
},
Question: []dns.Question{question},
}
disableCache := c.disableCache || options.DisableCache
if !disableCache {
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
if err != ErrNotCached {
return cachedAddresses, err
}
}
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
if err != nil {
return nil, err
@@ -430,98 +377,177 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
return MessageToAddresses(response), nil
}
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
response, _ := c.loadResponse(question, transport)
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := message.Question[0]
response, _, isStale := c.loadResponse(question, transport)
if response == nil {
return nil, ErrNotCached
}
if isStale {
if options.DisableOptimisticCache {
return nil, ErrNotCached
}
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
}
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
return MessageToAddresses(response), nil
}
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
var (
response *dns.Msg
loaded bool
)
if c.disableExpire {
if !c.independentCache {
response, loaded = c.cache.Get(question)
} else {
response, loaded = c.transportCache.Get(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if !loaded {
return nil, 0
}
return response.Copy(), 0
} else {
var expireAt time.Time
if !c.independentCache {
response, expireAt, loaded = c.cache.GetWithLifetime(question)
} else {
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
if !loaded {
return nil, 0
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if !c.independentCache {
c.cache.Remove(question)
} else {
c.transportCache.Remove(transportCacheKey{
Question: question,
transportTag: transport.Tag(),
})
}
return nil, 0
}
var originTTL int
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
originTTL = int(record.Header().Ttl)
}
}
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
if originTTL > 0 {
duration := uint32(originTTL - nowTTL)
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = record.Header().Ttl - duration
}
}
} else {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if record.Header().Rrtype == dns.TypeOPT {
continue
}
record.Header().Ttl = uint32(nowTTL)
}
}
}
return response, nowTTL
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
if c.dnsCache != nil {
return c.loadPersistentResponse(question, transport)
}
if c.cache == nil {
return nil, 0, false
}
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
if c.disableExpire {
response, loaded := c.cache.Get(key)
if !loaded {
return nil, 0, false
}
return response.Copy(), 0, false
}
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
if !loaded {
return nil, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
response = response.Copy()
normalizeTTL(response, 1)
return response, 0, true
}
c.cache.Remove(key)
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
response = response.Copy()
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
if !loaded {
return nil, 0, false
}
response := new(dns.Msg)
err := response.Unpack(rawMessage)
if err != nil {
return nil, 0, false
}
if c.disableExpire {
return response, 0, false
}
timeNow := time.Now()
if timeNow.After(expireAt) {
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
normalizeTTL(response, 1)
return response, 0, true
}
return nil, 0, false
}
nowTTL := int(expireAt.Sub(timeNow).Seconds())
if nowTTL < 0 {
nowTTL = 0
}
normalizeTTL(response, uint32(nowTTL))
return response, nowTTL, false
}
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
for _, rr := range response.Answer {
https, isHTTPS := rr.(*dns.HTTPS)
if !isHTTPS {
continue
}
content := https.SVCB
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
if options.Strategy == C.DomainStrategyIPv4Only {
return it.Key() != dns.SVCB_IPV6HINT
}
return it.Key() != dns.SVCB_IPV4HINT
})
https.SVCB = content
}
}
timeToLive := computeTimeToLive(response)
if options.RewriteTTL != nil {
timeToLive = *options.RewriteTTL
}
normalizeTTL(response, timeToLive)
return timeToLive
}
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
if loaded {
return
}
go func() {
defer c.backgroundRefresh.Delete(key)
ctx := contextWithTransportTag(c.ctx, transport.Tag())
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
if c.logger != nil {
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
}
return
}
if responseChecker != nil {
var rejected bool
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else {
rejected = !responseChecker(response)
}
if rejected {
if c.rdrc != nil {
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
}
return
}
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
return
}
timeToLive := applyResponseOptions(question, response, options)
c.storeCache(transport, question, response, timeToLive)
}()
}
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
clientSubnet := options.ClientSubnet
if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
}
return message
}
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
response, err := transport.Exchange(ctx, message)
if err == nil {
return response, nil
}
var rcodeError RcodeError
if errors.As(err, &rcodeError) {
return FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, err
}
func MessageToAddresses(response *dns.Msg) []netip.Addr {

View File

@@ -22,6 +22,19 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
}
}
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
if logger == nil || len(response.Question) == 0 {
return
}
domain := FqdnToDomain(response.Question[0].Name)
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
}
}
}
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
if logger == nil || len(response.Question) == 0 {
return

View File

@@ -51,7 +51,7 @@ type Router struct {
closing bool
}
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) *Router {
func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOptions) (*Router, error) {
router := &Router{
ctx: ctx,
logger: logFactory.NewLogger("dns"),
@@ -61,12 +61,30 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
rules: make([]adapter.DNSRule, 0, len(options.Rules)),
defaultDomainStrategy: C.DomainStrategy(options.Strategy),
}
if options.DNSClientOptions.IndependentCache {
deprecated.Report(ctx, deprecated.OptionIndependentDNSCache)
}
var optimisticTimeout time.Duration
optimisticOptions := common.PtrValueOrDefault(options.DNSClientOptions.Optimistic)
if optimisticOptions.Enabled {
if options.DNSClientOptions.DisableCache {
return nil, E.New("`optimistic` is conflict with `disable_cache`")
}
if options.DNSClientOptions.DisableExpire {
return nil, E.New("`optimistic` is conflict with `disable_expire`")
}
optimisticTimeout = time.Duration(optimisticOptions.Timeout)
if optimisticTimeout == 0 {
optimisticTimeout = 3 * 24 * time.Hour
}
}
router.client = NewClient(ClientOptions{
DisableCache: options.DNSClientOptions.DisableCache,
DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
Context: ctx,
DisableCache: options.DNSClientOptions.DisableCache,
DisableExpire: options.DNSClientOptions.DisableExpire,
OptimisticTimeout: optimisticTimeout,
CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
@@ -77,12 +95,24 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
}
return cacheFile
},
DNSCache: func() adapter.DNSCacheStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil {
return nil
}
if !cacheFile.StoreDNS() {
return nil
}
cacheFile.SetDisableExpire(options.DNSClientOptions.DisableExpire)
cacheFile.SetOptimisticTimeout(optimisticTimeout)
return cacheFile
},
Logger: router.logger,
})
if options.ReverseMapping {
router.dnsReverseMapping = common.Must1(freelru.NewSharded[netip.Addr, string](1024, maphash.NewHasher[netip.Addr]().Hash32))
}
return router
return router, nil
}
func (r *Router) Initialize(rules []option.DNSRule) error {
@@ -319,6 +349,9 @@ func (r *Router) applyDNSRouteOptions(options *adapter.DNSQueryOptions, routeOpt
if routeOptions.DisableCache {
options.DisableCache = true
}
if routeOptions.DisableOptimisticCache {
options.DisableOptimisticCache = true
}
if routeOptions.RewriteTTL != nil {
options.RewriteTTL = routeOptions.RewriteTTL
}
@@ -907,7 +940,9 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
return dnsRuleModeRequirementsInDefaultRule(router, rule.DefaultOptions, metadataOverrides)
case C.RuleTypeLogical:
flags := dnsRuleModeFlags{
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate || dnsRuleActionType(rule) == C.RuleActionTypeRespond,
disabled: dnsRuleActionType(rule) == C.RuleActionTypeEvaluate ||
dnsRuleActionType(rule) == C.RuleActionTypeRespond ||
dnsRuleActionDisablesLegacyDNSMode(rule.LogicalOptions.DNSRuleAction),
neededFromStrategy: dnsRuleActionHasStrategy(rule.LogicalOptions.DNSRuleAction),
}
flags.needed = flags.neededFromStrategy
@@ -926,7 +961,7 @@ func dnsRuleModeRequirementsInRule(router adapter.Router, rule option.DNSRule, m
func dnsRuleModeRequirementsInDefaultRule(router adapter.Router, rule option.DefaultDNSRule, metadataOverrides map[string]adapter.RuleSetMetadata) (dnsRuleModeFlags, error) {
flags := dnsRuleModeFlags{
disabled: defaultRuleDisablesLegacyDNSMode(rule),
disabled: defaultRuleDisablesLegacyDNSMode(rule) || dnsRuleActionDisablesLegacyDNSMode(rule.DNSRuleAction),
neededFromStrategy: dnsRuleActionHasStrategy(rule.DNSRuleAction),
}
flags.needed = defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule) || flags.neededFromStrategy
@@ -1063,6 +1098,17 @@ func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool,
return rule.MatchResponse || rule.Action == C.RuleActionTypeRespond, nil
}
func dnsRuleActionDisablesLegacyDNSMode(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:
return action.RouteOptions.DisableOptimisticCache
case C.RuleActionTypeRouteOptions:
return action.RouteOptionsOptions.DisableOptimisticCache
default:
return false
}
}
func dnsRuleActionHasStrategy(action option.DNSRuleAction) bool {
switch action.Action {
case "", C.RuleActionTypeRoute, C.RuleActionTypeEvaluate:

View File

@@ -4,8 +4,24 @@ package local
/*
#include <stdlib.h>
#include <dns.h>
#include <resolv.h>
#include <netdb.h>
static void *cgo_dns_open_super() {
return (void *)dns_open(NULL);
}
static void cgo_dns_close(void *opaque) {
if (opaque != NULL) dns_free((dns_handle_t)opaque);
}
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
unsigned char *answer, int anslen) {
dns_handle_t handle = (dns_handle_t)opaque;
struct sockaddr_storage from;
uint32_t fromlen = sizeof(from);
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
}
static void *cgo_res_init() {
res_state state = calloc(1, sizeof(struct __res_state));
@@ -52,7 +68,59 @@ import (
mDNS "github.com/miekg/dns"
)
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
const (
darwinResolverHostNotFound = 1
darwinResolverTryAgain = 2
darwinResolverNoRecovery = 3
darwinResolverNoData = 4
darwinResolverMaxPacketSize = 65535
)
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
response, err := darwinSearchWithSystemRouting(name, class, qtype)
if err == nil {
return response, nil
}
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
if fallbackErr == nil || fallbackResponse != nil {
return fallbackResponse, fallbackErr
}
return nil, E.Errors(
E.Cause(err, "dns_search"),
E.Cause(fallbackErr, "res_nsearch"),
)
}
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
handle := C.cgo_dns_open_super()
if handle == nil {
return nil, E.New("dns_open failed")
}
defer C.cgo_dns_close(handle)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
if n <= 0 {
return nil, E.New("dns_search failed for ", name)
}
if int(n) > bufSize {
bufSize = int(n)
continue
}
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
}
}
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
state := C.cgo_res_init()
if state == nil {
return nil, E.New("res_ninit failed")
@@ -61,6 +129,7 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg,
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
@@ -74,37 +143,55 @@ func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg,
bufSize = int(n)
continue
}
var response mDNS.Msg
err := response.Unpack(answer[:int(n)])
if err != nil {
return nil, E.Cause(err, "unpack res_nsearch response")
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
}
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
if err == nil {
return response, nil
}
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
bufSize *= 2
if bufSize > darwinResolverMaxPacketSize {
bufSize = darwinResolverMaxPacketSize
}
return &response, nil
continue
}
var response mDNS.Msg
_ = response.Unpack(answer[:bufSize])
if response.Response {
if response.Truncated && bufSize < 65535 {
bufSize *= 2
if bufSize > 65535 {
bufSize = 65535
}
continue
}
return &response, nil
}
switch hErrno {
case C.HOST_NOT_FOUND:
return nil, dns.RcodeNameError
case C.TRY_AGAIN:
return nil, dns.RcodeNameError
case C.NO_RECOVERY:
return nil, dns.RcodeServerFailure
case C.NO_DATA:
return nil, dns.RcodeSuccess
default:
return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name)
return nil, err
}
}
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
var response mDNS.Msg
err := response.Unpack(packet)
if err != nil {
return nil, E.Cause(err, "unpack ", source, " response")
}
return &response, nil
}
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
if err == nil && response.Response {
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
return nil, errDarwinNeedLargerBuffer
}
return response, nil
}
return nil, darwinResolverHErrno(name, hErrno)
}
func darwinResolverHErrno(name string, hErrno int) error {
switch hErrno {
case darwinResolverHostNotFound:
return dns.RcodeNameError
case darwinResolverTryAgain:
return dns.RcodeServerFailure
case darwinResolverNoRecovery:
return dns.RcodeServerFailure
case darwinResolverNoData:
return dns.RcodeSuccess
default:
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
}
}
@@ -141,7 +228,7 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
}
resultCh := make(chan resolvResult, 1)
go func() {
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
resultCh <- resolvResult{response, err}
}()
var result resolvResult

View File

@@ -2,11 +2,75 @@
icon: material/alert-decagram
---
#### 1.14.0-alpha.10
* Add `evaluate` DNS rule action and Response Match Fields **1**
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
* Add `package_name_regex` route, DNS and headless rule item **3**
* Add cloudflared inbound **4**
* Fixes and improvements
**1**:
Response Match Fields
([`response_rcode`](/configuration/dns/rule/#response_rcode),
[`response_answer`](/configuration/dns/rule/#response_answer),
[`response_ns`](/configuration/dns/rule/#response_ns),
and [`response_extra`](/configuration/dns/rule/#response_extra))
match the evaluated DNS response. They are gated by the new
[`match_response`](/configuration/dns/rule/#match_response) field and
populated by a preceding
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
the evaluated response can also be returned directly by a
[`respond`](/configuration/dns/rule_action/#respond) action.
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
`ip_is_private` without `match_response`) in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
in sing-box 1.16.0.
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
**2**:
`ip_version` and `query_type` in DNS rules, together with `query_type` in
referenced rule-sets, now take effect on every DNS rule evaluation,
including matches from internal domain resolutions that do not target a
specific DNS server (for example a `resolve` route rule action without
`server` set). In earlier versions they were silently ignored in that
path. Combining these fields with any of the legacy DNS fields deprecated
in **1** in the same DNS configuration is no longer supported and is
rejected at startup.
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
**3**:
See [Route Rule](/configuration/route/rule/#package_name_regex),
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
**4**:
See [Cloudflared](/configuration/inbound/cloudflared/).
#### 1.13.7
* Fixes and improvement
#### 1.13.6
* Fixes and improvements
#### 1.13.6
#### 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
@@ -14,10 +78,33 @@ icon: material/alert-decagram
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements
#### 1.13.4
* Fixes and improvements
#### 1.14.0-alpha.4
* Refactor ACME support to certificate provider system **1**
* Add Cloudflare Origin CA certificate provider **2**
* Add Tailscale certificate provider **3**
* Fixes and improvements
**1**:
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
**2**:
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
**3**:
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
#### 1.13.3
* Add OpenWrt and Alpine APK packages to release **1**
@@ -42,6 +129,59 @@ from [SagerNet/go](https://github.com/SagerNet/go).
See [OCM](/configuration/service/ocm).
#### 1.12.24
* Fixes and improvements
#### 1.14.0-alpha.2
* Add OpenWrt and Alpine APK packages to release **1**
* Backport to macOS 10.13 High Sierra **2**
* OCM service: Add WebSocket support for Responses API **3**
* Fixes and improvements
**1**:
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
**2**:
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
macOS 10.13 High Sierra, built using Go 1.25 with patches
from [SagerNet/go](https://github.com/SagerNet/go).
**3**:
See [OCM](/configuration/service/ocm).
#### 1.14.0-alpha.1
* Add `source_mac_address` and `source_hostname` rule items **1**
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
* Update NaiveProxy to 145.0.7632.159 **3**
* Fixes and improvements
**1**:
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
Supported on Linux, macOS, or in graphical clients on Android and macOS.
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
**2**:
Limit or exclude devices from TUN routing by MAC address.
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
See [TUN](/configuration/inbound/tun/#include_mac_address).
**3**:
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
#### 1.13.2
* Fixes and improvements

View File

@@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [independent_cache](#independent_cache)
:material-plus: [optimistic](#optimistic)
!!! quote "Changes in sing-box 1.12.0"
:material-decagram: [servers](#servers)
@@ -25,6 +30,7 @@ icon: material/alert-decagram
"disable_expire": false,
"independent_cache": false,
"cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
@@ -57,12 +63,20 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Disable dns cache.
Conflict with `optimistic`.
#### disable_expire
Disable dns cache expire.
Conflict with `optimistic`.
#### independent_cache
!!! failure "Deprecated in sing-box 1.14.0"
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
#### cache_capacity
@@ -73,6 +87,34 @@ LRU cache capacity.
Value less than 1024 will be ignored.
#### optimistic
!!! question "Since sing-box 1.14.0"
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
the stale response is returned immediately while a background refresh is triggered.
Conflict with `disable_cache` and `disable_expire`.
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
```json
{
"enabled": true,
"timeout": "3d"
}
```
##### enabled
Enable optimistic DNS caching.
##### timeout
The maximum time an expired cache entry can be served optimistically.
`3d` is used by default.
#### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.

View File

@@ -2,6 +2,11 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [independent_cache](#independent_cache)
:material-plus: [optimistic](#optimistic)
!!! quote "sing-box 1.12.0 中的更改"
:material-decagram: [servers](#servers)
@@ -25,6 +30,7 @@ icon: material/alert-decagram
"disable_expire": false,
"independent_cache": false,
"cache_capacity": 0,
"optimistic": false, // or {}
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
@@ -56,12 +62,20 @@ icon: material/alert-decagram
禁用 DNS 缓存。
`optimistic` 冲突。
#### disable_expire
禁用 DNS 缓存过期。
`optimistic` 冲突。
#### independent_cache
!!! failure "已在 sing-box 1.14.0 废弃"
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
#### cache_capacity
@@ -72,6 +86,34 @@ LRU 缓存容量。
小于 1024 的值将被忽略。
#### optimistic
!!! question "自 sing-box 1.14.0 起"
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
立即返回过期的响应,同时在后台触发刷新。
`disable_cache``disable_expire` 冲突。
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`
```json
{
"enabled": true,
"timeout": "3d"
}
```
##### enabled
启用乐观 DNS 缓存。
##### timeout
过期缓存条目可被乐观提供的最长时间。
默认使用 `3d`
#### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。

View File

@@ -12,7 +12,9 @@ icon: material/alert-decagram
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
!!! quote "Changes in sing-box 1.13.0"
@@ -243,12 +245,46 @@ Tags of [Inbound](/configuration/inbound/).
#### ip_version
!!! quote "Changes in sing-box 1.14.0"
This field now also applies when a DNS rule is matched from an internal
domain resolution that does not target a specific DNS server, such as a
[`resolve`](../../route/rule_action/#resolve) route rule action without a
`server` set. In earlier versions, only DNS queries received from a
client evaluated this field. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
for the full list.
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
action and [`match_response`](#match_response).
4 (A DNS query) or 6 (AAAA DNS query).
Not limited if empty.
#### query_type
!!! quote "Changes in sing-box 1.14.0"
This field now also applies when a DNS rule is matched from an internal
domain resolution that does not target a specific DNS server, such as a
[`resolve`](../../route/rule_action/#resolve) route rule action without a
`server` set. In earlier versions, only DNS queries received from a
client evaluated this field. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
for the full list.
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
`strategy` DNS rule action option, and the Legacy
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
action and [`match_response`](#match_response).
DNS query type. Values can be integers or type name strings.
#### network

View File

@@ -12,7 +12,9 @@ icon: material/alert-decagram
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
!!! quote "sing-box 1.13.0 中的更改"
@@ -243,12 +245,38 @@ icon: material/alert-decagram
#### ip_version
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
[`match_response`](#match_response)。
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
默认不限制。
#### query_type
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
[`match_response`](#match_response)。
DNS 查询类型。值可以为整数或者类型名称字符串。
#### network

View File

@@ -6,7 +6,8 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "Changes in sing-box 1.12.0"
@@ -23,6 +24,7 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -52,6 +54,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
@@ -73,6 +81,7 @@ Will override `dns.client_subnet`.
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -97,6 +106,12 @@ Tag of target server.
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
@@ -131,6 +146,7 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}

View File

@@ -6,7 +6,8 @@ icon: material/new-box
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "sing-box 1.12.0 中的更改"
@@ -23,6 +24,7 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -52,6 +54,12 @@ icon: material/new-box
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
@@ -73,6 +81,7 @@ icon: material/new-box
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -95,6 +104,12 @@ icon: material/new-box
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
@@ -129,6 +144,7 @@ icon: material/new-box
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}

View File

@@ -1,5 +1,10 @@
!!! question "Since sing-box 1.8.0"
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [store_rdrc](#store_rdrc)
:material-plus: [store_dns](#store_dns)
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [store_rdrc](#store_rdrc)
@@ -14,7 +19,8 @@
"cache_id": "",
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": ""
"rdrc_timeout": "",
"store_dns": false
}
```
@@ -42,6 +48,10 @@ Store fakeip in the cache file
#### store_rdrc
!!! failure "Deprecated in sing-box 1.14.0"
`store_rdrc` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-store-rdrc).
Store rejected DNS response cache in the cache file
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
@@ -52,3 +62,9 @@ will be cached until expiration.
Timeout of rejected DNS response cache.
`7d` is used by default.
#### store_dns
!!! question "Since sing-box 1.14.0"
Store DNS cache in the cache file.

View File

@@ -1,5 +1,10 @@
!!! question "自 sing-box 1.8.0 起"
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [store_rdrc](#store_rdrc)
:material-plus: [store_dns](#store_dns)
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [store_rdrc](#store_rdrc)
@@ -14,7 +19,8 @@
"cache_id": "",
"store_fakeip": false,
"store_rdrc": false,
"rdrc_timeout": ""
"rdrc_timeout": "",
"store_dns": false
}
```
@@ -40,6 +46,10 @@
#### store_rdrc
!!! failure "已在 sing-box 1.14.0 废弃"
`store_rdrc` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
将拒绝的 DNS 响应缓存存储在缓存文件中。
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
@@ -49,3 +59,9 @@
拒绝的 DNS 响应缓存超时。
默认使用 `7d`
#### store_dns
!!! question "自 sing-box 1.14.0 起"
将 DNS 缓存存储在缓存文件中。

View File

@@ -7,6 +7,10 @@ icon: material/new-box
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject)
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [tls_fragment](#tls_fragment)
@@ -279,6 +283,7 @@ Timeout for sniffing.
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -302,6 +307,12 @@ DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ip
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
!!! question "Since sing-box 1.12.0"

View File

@@ -7,6 +7,10 @@ icon: material/new-box
:material-plus: [bypass](#bypass)
:material-alert: [reject](#reject)
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [resolve.disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment)
@@ -268,6 +272,7 @@ UDP 连接超时时间。
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -291,6 +296,12 @@ DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
!!! question "自 sing-box 1.12.0 起"

View File

@@ -4,7 +4,8 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [query_type](#query_type)
!!! quote "Changes in sing-box 1.13.0"
@@ -132,6 +133,20 @@ icon: material/new-box
#### query_type
!!! quote "Changes in sing-box 1.14.0"
When a DNS rule references this rule-set, this field now also applies
when the DNS rule is matched from an internal domain resolution that
does not target a specific DNS server. In earlier versions, only DNS
queries received from a client evaluated this field. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
for the full list.
When a DNS rule references a rule-set containing this field, the DNS
rule is incompatible in the same DNS configuration with Legacy Address
Filter Fields in DNS rules, the Legacy `strategy` DNS rule action
option, and the Legacy `rule_set_ip_cidr_accept_empty` DNS rule item.
DNS query type. Values can be integers or type name strings.
#### network

View File

@@ -4,7 +4,8 @@ icon: material/new-box
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [package_name_regex](#package_name_regex)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [query_type](#query_type)
!!! quote "sing-box 1.13.0 中的更改"
@@ -132,6 +133,17 @@ icon: material/new-box
#### query_type
!!! quote "sing-box 1.14.0 中的更改"
当 DNS 规则引用此规则集时,此字段现在也会在 DNS 规则被未指定具体
DNS 服务器的内部域名解析匹配时生效。此前只有来自客户端的 DNS 查询
才会评估此字段。完整列表参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
当 DNS 规则引用了包含此字段的规则集时,该 DNS 规则在同一 DNS 配置中
不能与旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。
DNS 查询类型。值可以为整数或者类型名称字符串。
#### network

View File

@@ -27,6 +27,21 @@ check [Migration](../migration/#migrate-address-filter-fields-to-response-matchi
Old fields will be removed in sing-box 1.16.0.
#### `independent_cache` DNS option
`independent_cache` DNS option is deprecated.
The DNS cache now always keys by transport, making this option unnecessary,
check [Migration](../migration/#migrate-independent-dns-cache).
Old fields will be removed in sing-box 1.16.0.
#### `store_rdrc` cache file option
`store_rdrc` cache file option is deprecated,
check [Migration](../migration/#migrate-store-rdrc).
Old fields will be removed in sing-box 1.16.0.
#### Legacy Address Filter Fields in DNS rules
Legacy Address Filter Fields (`ip_cidr`, `ip_is_private` without `match_response`)

View File

@@ -27,6 +27,21 @@ TLS 中的内联 ACME 选项(`tls.acme`)已废弃,
旧字段将在 sing-box 1.16.0 中被移除。
#### `independent_cache` DNS 选项
`independent_cache` DNS 选项已废弃。
DNS 缓存现在始终按传输分离,使此选项不再需要,
参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
旧字段将在 sing-box 1.16.0 中被移除。
#### `store_rdrc` 缓存文件选项
`store_rdrc` 缓存文件选项已废弃,
参阅[迁移指南](/zh/migration/#迁移-store_rdrc)。
旧字段将在 sing-box 1.16.0 中被移除。
#### 旧版地址筛选字段 (DNS 规则)
旧版地址筛选字段(不使用 `match_response``ip_cidr``ip_is_private`)已废弃,

View File

@@ -137,6 +137,118 @@ to fetch a DNS response, then match against it explicitly with `match_response`.
}
```
### Migrate independent DNS cache
The DNS cache now always keys by transport name, making `independent_cache` unnecessary.
Simply remove the field.
!!! info "References"
[DNS](/configuration/dns/)
=== ":material-card-remove: Deprecated"
```json
{
"dns": {
"independent_cache": true
}
}
```
=== ":material-card-multiple: New"
```json
{
"dns": {}
}
```
### Migrate store_rdrc
`store_rdrc` is deprecated and can be replaced by `store_dns`,
which persists the full DNS cache to the cache file.
!!! info "References"
[Cache File](/configuration/experimental/cache-file/)
=== ":material-card-remove: Deprecated"
```json
{
"experimental": {
"cache_file": {
"enabled": true,
"store_rdrc": true
}
}
}
```
=== ":material-card-multiple: New"
```json
{
"experimental": {
"cache_file": {
"enabled": true,
"store_dns": true
}
}
}
```
### ip_version and query_type behavior changes in DNS rules
In sing-box 1.14.0, the behavior of
[`ip_version`](/configuration/dns/rule/#ip_version) and
[`query_type`](/configuration/dns/rule/#query_type) in DNS rules, together with
[`query_type`](/configuration/rule-set/headless-rule/#query_type) in referenced
rule-sets, changes in two ways.
First, these fields now take effect on every DNS rule evaluation. In earlier
versions they were evaluated only for DNS queries received from a client
(for example, from a DNS inbound or intercepted by `tun`), and were silently
ignored when a DNS rule was matched from an internal domain resolution that
did not target a specific DNS server. Such internal resolutions include:
- The [`resolve`](/configuration/route/rule_action/#resolve) route rule
action without a `server` set.
- ICMP traffic routed to a domain destination through a `direct` outbound.
- A [WireGuard](/configuration/endpoint/wireguard/) or
[Tailscale](/configuration/endpoint/tailscale/) endpoint used as an
outbound, when resolving its own destination address.
- A [SOCKS4](/configuration/outbound/socks/) outbound, which must resolve
the destination locally because the protocol has no in-protocol domain
support.
- The [DERP](/configuration/service/derp/) `bootstrap-dns` endpoint and the
[`resolved`](/configuration/service/resolved/) service (when resolving a
hostname or an SRV target).
Resolutions that target a specific DNS server — via
[`domain_resolver`](/configuration/shared/dial/#domain_resolver) on a dial
field, [`default_domain_resolver`](/configuration/route/#default_domain_resolver)
in route options, or an explicit `server` on a DNS rule action or the
`resolve` route rule action — do not go through DNS rule matching and are
unaffected.
Second, setting `ip_version` or `query_type` in a DNS rule, or referencing a
rule-set containing `query_type`, is no longer compatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
`strategy` DNS rule action option, or the Legacy `rule_set_ip_cidr_accept_empty`
DNS rule item. Such a configuration will be rejected at startup. To combine
these fields with address-based filtering, migrate to response matching via
the [`evaluate`](/configuration/dns/rule_action/#evaluate) action and
[`match_response`](/configuration/dns/rule/#match_response), see
[Migrate address filter fields to response matching](#migrate-address-filter-fields-to-response-matching).
!!! info "References"
[DNS Rule](/configuration/dns/rule/) /
[Headless Rule](/configuration/rule-set/headless-rule/) /
[Route Rule Action](/configuration/route/rule_action/#resolve)
## 1.12.0
### Migrate to new DNS server formats

View File

@@ -137,6 +137,109 @@ sing-box 1.14.0 新增字段参阅 [ACME](/zh/configuration/shared/certificate-p
}
```
### 迁移 independent DNS cache
DNS 缓存现在始终按传输名称分离,使 `independent_cache` 不再需要。
直接移除该字段即可。
!!! info "参考"
[DNS](/zh/configuration/dns/)
=== ":material-card-remove: 弃用的"
```json
{
"dns": {
"independent_cache": true
}
}
```
=== ":material-card-multiple: 新的"
```json
{
"dns": {}
}
```
### 迁移 store_rdrc
`store_rdrc` 已废弃,且可以被 `store_dns` 替代,
后者将完整的 DNS 缓存持久化到缓存文件中。
!!! info "参考"
[缓存文件](/zh/configuration/experimental/cache-file/)
=== ":material-card-remove: 弃用的"
```json
{
"experimental": {
"cache_file": {
"enabled": true,
"store_rdrc": true
}
}
}
```
=== ":material-card-multiple: 新的"
```json
{
"experimental": {
"cache_file": {
"enabled": true,
"store_dns": true
}
}
}
```
### DNS 规则中的 ip_version 和 query_type 行为更改
在 sing-box 1.14.0 中DNS 规则中的
[`ip_version`](/zh/configuration/dns/rule/#ip_version) 和
[`query_type`](/zh/configuration/dns/rule/#query_type),以及被引用规则集中的
[`query_type`](/zh/configuration/rule-set/headless-rule/#query_type)
行为有两项更改。
其一,这些字段现在对每一次 DNS 规则评估都会生效。此前它们仅对来自客户端的 DNS 查询
(例如来自 DNS 入站或被 `tun` 截获的查询)生效,当 DNS 规则被未指定具体 DNS 服务器的
内部域名解析匹配时,会被静默忽略。此类内部解析包括:
- 未设置 `server` 的 [`resolve`](/zh/configuration/route/rule_action/#resolve) 路由规则动作。
- 通过 `direct` 出站路由到域名目标的 ICMP 流量。
- 作为出站使用的 [WireGuard](/zh/configuration/endpoint/wireguard/) 或
[Tailscale](/zh/configuration/endpoint/tailscale/) 端点在解析自身目标地址时。
- [SOCKS4](/zh/configuration/outbound/socks/) 出站,因为协议本身不支持域名,
必须在本地解析目标。
- [DERP](/zh/configuration/service/derp/) 的 `bootstrap-dns` 端点,以及
[`resolved`](/zh/configuration/service/resolved/) 服务在解析主机名或 SRV 目标时。
通过拨号字段中的
[`domain_resolver`](/zh/configuration/shared/dial/#domain_resolver)、
路由选项中的 [`default_domain_resolver`](/zh/configuration/route/#default_domain_resolver)
或 DNS 规则动作与 `resolve` 路由规则动作上显式的 `server` 指定具体 DNS 服务器的
解析,不会经过 DNS 规则匹配,不受此次更改影响。
其二,设置了 `ip_version` 或 `query_type` 的 DNS 规则,或引用了包含 `query_type` 的
规则集的 DNS 规则,在同一 DNS 配置中不再能与旧版地址筛选字段 (DNS 规则)、旧版
DNS 规则动作 `strategy` 选项,或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。
此类配置将在启动时被拒绝。如需将这些字段与基于地址的筛选组合,请通过
[`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作和
[`match_response`](/zh/configuration/dns/rule/#match_response) 迁移到响应匹配,
参阅 [迁移地址筛选字段到响应匹配](#迁移地址筛选字段到响应匹配)。
!!! info "参考"
[DNS 规则](/zh/configuration/dns/rule/) /
[Headless 规则](/zh/configuration/rule-set/headless-rule/) /
[路由规则动作](/zh/configuration/route/rule_action/#resolve)
## 1.12.0
### 迁移到新的 DNS 服务器格式

View File

@@ -12,9 +12,11 @@ import (
"github.com/sagernet/bbolt"
bboltErrors "github.com/sagernet/bbolt/errors"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service/filemanager"
)
@@ -30,6 +32,7 @@ var (
string(bucketMode),
string(bucketRuleSet),
string(bucketRDRC),
string(bucketDNSCache),
}
cacheIDDefault = []byte("default")
@@ -38,30 +41,43 @@ var (
var _ adapter.CacheFile = (*CacheFile)(nil)
type CacheFile struct {
ctx context.Context
path string
cacheID []byte
storeFakeIP bool
storeRDRC bool
rdrcTimeout time.Duration
DB *bbolt.DB
resetAccess sync.Mutex
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
saveRDRCAccess sync.RWMutex
saveRDRC map[saveRDRCCacheKey]bool
ctx context.Context
logger logger.Logger
path string
cacheID []byte
storeFakeIP bool
storeRDRC bool
storeDNS bool
disableExpire bool
rdrcTimeout time.Duration
optimisticTimeout time.Duration
DB *bbolt.DB
resetAccess sync.Mutex
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
saveRDRCAccess sync.RWMutex
saveRDRC map[saveCacheKey]bool
saveDNSCacheAccess sync.RWMutex
saveDNSCache map[saveCacheKey]saveDNSCacheEntry
}
type saveRDRCCacheKey struct {
type saveCacheKey struct {
TransportName string
QuestionName string
QType uint16
}
func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
type saveDNSCacheEntry struct {
rawMessage []byte
expireAt time.Time
sequence uint64
saving bool
}
func New(ctx context.Context, logger logger.Logger, options option.CacheFileOptions) *CacheFile {
var path string
if options.Path != "" {
path = options.Path
@@ -72,6 +88,9 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
if options.CacheID != "" {
cacheIDBytes = append([]byte{0}, []byte(options.CacheID)...)
}
if options.StoreRDRC {
deprecated.Report(ctx, deprecated.OptionStoreRDRC)
}
var rdrcTimeout time.Duration
if options.StoreRDRC {
if options.RDRCTimeout > 0 {
@@ -82,15 +101,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
}
return &CacheFile{
ctx: ctx,
logger: logger,
path: filemanager.BasePath(ctx, path),
cacheID: cacheIDBytes,
storeFakeIP: options.StoreFakeIP,
storeRDRC: options.StoreRDRC,
storeDNS: options.StoreDNS,
rdrcTimeout: rdrcTimeout,
saveDomain: make(map[netip.Addr]string),
saveAddress4: make(map[string]netip.Addr),
saveAddress6: make(map[string]netip.Addr),
saveRDRC: make(map[saveRDRCCacheKey]bool),
saveRDRC: make(map[saveCacheKey]bool),
saveDNSCache: make(map[saveCacheKey]saveDNSCacheEntry),
}
}
@@ -102,10 +124,44 @@ func (c *CacheFile) Dependencies() []string {
return nil
}
func (c *CacheFile) SetOptimisticTimeout(timeout time.Duration) {
c.optimisticTimeout = timeout
}
func (c *CacheFile) SetDisableExpire(disableExpire bool) {
c.disableExpire = disableExpire
}
func (c *CacheFile) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
switch stage {
case adapter.StartStateInitialize:
return c.start()
case adapter.StartStateStart:
c.startCacheCleanup()
}
return nil
}
func (c *CacheFile) startCacheCleanup() {
if c.storeDNS {
c.clearRDRC()
c.cleanupDNSCache()
interval := c.optimisticTimeout / 2
if interval <= 0 {
interval = time.Hour
}
go c.loopCacheCleanup(interval, c.cleanupDNSCache)
} else if c.storeRDRC {
c.cleanupRDRC()
interval := c.rdrcTimeout / 2
if interval <= 0 {
interval = time.Hour
}
go c.loopCacheCleanup(interval, c.cleanupRDRC)
}
}
func (c *CacheFile) start() error {
const fileMode = 0o666
options := bbolt.Options{Timeout: time.Second}
var (

View File

@@ -0,0 +1,299 @@
package cachefile
import (
"encoding/binary"
"time"
"github.com/sagernet/bbolt"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/logger"
)
var bucketDNSCache = []byte("dns_cache")
func (c *CacheFile) StoreDNS() bool {
return c.storeDNS
}
func (c *CacheFile) LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool) {
c.saveDNSCacheAccess.RLock()
entry, cached := c.saveDNSCache[saveCacheKey{transportName, qName, qType}]
c.saveDNSCacheAccess.RUnlock()
if cached {
return entry.rawMessage, entry.expireAt, true
}
key := buf.Get(2 + len(qName))
binary.BigEndian.PutUint16(key, qType)
copy(key[2:], qName)
defer buf.Put(key)
err := c.view(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketDNSCache)
if bucket == nil {
return nil
}
bucket = bucket.Bucket([]byte(transportName))
if bucket == nil {
return nil
}
content := bucket.Get(key)
if len(content) < 8 {
return nil
}
expireAt = time.Unix(int64(binary.BigEndian.Uint64(content[:8])), 0)
rawMessage = make([]byte, len(content)-8)
copy(rawMessage, content[8:])
loaded = true
return nil
})
if err != nil {
return nil, time.Time{}, false
}
return
}
func (c *CacheFile) SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error {
return c.batch(func(tx *bbolt.Tx) error {
bucket, err := c.createBucket(tx, bucketDNSCache)
if err != nil {
return err
}
bucket, err = bucket.CreateBucketIfNotExists([]byte(transportName))
if err != nil {
return err
}
key := buf.Get(2 + len(qName))
binary.BigEndian.PutUint16(key, qType)
copy(key[2:], qName)
defer buf.Put(key)
value := buf.Get(8 + len(rawMessage))
defer buf.Put(value)
binary.BigEndian.PutUint64(value[:8], uint64(expireAt.Unix()))
copy(value[8:], rawMessage)
return bucket.Put(key, value)
})
}
func (c *CacheFile) SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger) {
saveKey := saveCacheKey{transportName, qName, qType}
if !c.queueDNSCacheSave(saveKey, rawMessage, expireAt) {
return
}
go c.flushPendingDNSCache(saveKey, logger)
}
func (c *CacheFile) queueDNSCacheSave(saveKey saveCacheKey, rawMessage []byte, expireAt time.Time) bool {
c.saveDNSCacheAccess.Lock()
defer c.saveDNSCacheAccess.Unlock()
entry := c.saveDNSCache[saveKey]
entry.rawMessage = append([]byte(nil), rawMessage...)
entry.expireAt = expireAt
entry.sequence++
startFlush := !entry.saving
entry.saving = true
c.saveDNSCache[saveKey] = entry
return startFlush
}
func (c *CacheFile) flushPendingDNSCache(saveKey saveCacheKey, logger logger.Logger) {
c.flushPendingDNSCacheWith(saveKey, logger, func(entry saveDNSCacheEntry) error {
return c.SaveDNSCache(saveKey.TransportName, saveKey.QuestionName, saveKey.QType, entry.rawMessage, entry.expireAt)
})
}
func (c *CacheFile) flushPendingDNSCacheWith(saveKey saveCacheKey, logger logger.Logger, save func(saveDNSCacheEntry) error) {
for {
c.saveDNSCacheAccess.RLock()
entry, loaded := c.saveDNSCache[saveKey]
c.saveDNSCacheAccess.RUnlock()
if !loaded {
return
}
err := save(entry)
if err != nil {
logger.Warn("save DNS cache: ", err)
}
c.saveDNSCacheAccess.Lock()
currentEntry, loaded := c.saveDNSCache[saveKey]
if !loaded {
c.saveDNSCacheAccess.Unlock()
return
}
if currentEntry.sequence != entry.sequence {
c.saveDNSCacheAccess.Unlock()
continue
}
delete(c.saveDNSCache, saveKey)
c.saveDNSCacheAccess.Unlock()
return
}
}
func (c *CacheFile) ClearDNSCache() error {
c.saveDNSCacheAccess.Lock()
clear(c.saveDNSCache)
c.saveDNSCacheAccess.Unlock()
return c.batch(func(tx *bbolt.Tx) error {
if c.cacheID == nil {
bucket := tx.Bucket(bucketDNSCache)
if bucket == nil {
return nil
}
return tx.DeleteBucket(bucketDNSCache)
}
bucket := tx.Bucket(c.cacheID)
if bucket == nil || bucket.Bucket(bucketDNSCache) == nil {
return nil
}
return bucket.DeleteBucket(bucketDNSCache)
})
}
func (c *CacheFile) loopCacheCleanup(interval time.Duration, cleanupFunc func()) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ticker.C:
cleanupFunc()
}
}
}
func (c *CacheFile) cleanupDNSCache() {
now := time.Now()
err := c.batch(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketDNSCache)
if bucket == nil {
return nil
}
var emptyTransports [][]byte
err := bucket.ForEachBucket(func(transportName []byte) error {
transportBucket := bucket.Bucket(transportName)
if transportBucket == nil {
return nil
}
var expiredKeys [][]byte
err := transportBucket.ForEach(func(key, value []byte) error {
if len(value) < 8 {
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
return nil
}
if c.disableExpire {
return nil
}
expireAt := time.Unix(int64(binary.BigEndian.Uint64(value[:8])), 0)
if now.After(expireAt.Add(c.optimisticTimeout)) {
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
}
return nil
})
if err != nil {
return err
}
for _, key := range expiredKeys {
err = transportBucket.Delete(key)
if err != nil {
return err
}
}
first, _ := transportBucket.Cursor().First()
if first == nil {
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
}
return nil
})
if err != nil {
return err
}
for _, name := range emptyTransports {
err = bucket.DeleteBucket(name)
if err != nil {
return err
}
}
return nil
})
if err != nil {
c.logger.Warn("cleanup DNS cache: ", err)
}
}
func (c *CacheFile) clearRDRC() {
c.saveRDRCAccess.Lock()
clear(c.saveRDRC)
c.saveRDRCAccess.Unlock()
err := c.batch(func(tx *bbolt.Tx) error {
if c.cacheID == nil {
if tx.Bucket(bucketRDRC) == nil {
return nil
}
return tx.DeleteBucket(bucketRDRC)
}
bucket := tx.Bucket(c.cacheID)
if bucket == nil || bucket.Bucket(bucketRDRC) == nil {
return nil
}
return bucket.DeleteBucket(bucketRDRC)
})
if err != nil {
c.logger.Warn("clear RDRC: ", err)
}
}
func (c *CacheFile) cleanupRDRC() {
now := time.Now()
err := c.batch(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketRDRC)
if bucket == nil {
return nil
}
var emptyTransports [][]byte
err := bucket.ForEachBucket(func(transportName []byte) error {
transportBucket := bucket.Bucket(transportName)
if transportBucket == nil {
return nil
}
var expiredKeys [][]byte
err := transportBucket.ForEach(func(key, value []byte) error {
if len(value) < 8 {
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
return nil
}
expiresAt := time.Unix(int64(binary.BigEndian.Uint64(value)), 0)
if now.After(expiresAt) {
expiredKeys = append(expiredKeys, append([]byte(nil), key...))
}
return nil
})
if err != nil {
return err
}
for _, key := range expiredKeys {
err = transportBucket.Delete(key)
if err != nil {
return err
}
}
first, _ := transportBucket.Cursor().First()
if first == nil {
emptyTransports = append(emptyTransports, append([]byte(nil), transportName...))
}
return nil
})
if err != nil {
return err
}
for _, name := range emptyTransports {
err = bucket.DeleteBucket(name)
if err != nil {
return err
}
}
return nil
})
if err != nil {
c.logger.Warn("cleanup RDRC: ", err)
}
}

View File

@@ -21,7 +21,7 @@ func (c *CacheFile) RDRCTimeout() time.Duration {
func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (rejected bool) {
c.saveRDRCAccess.RLock()
rejected, cached := c.saveRDRC[saveRDRCCacheKey{transportName, qName, qType}]
rejected, cached := c.saveRDRC[saveCacheKey{transportName, qName, qType}]
c.saveRDRCAccess.RUnlock()
if cached {
return
@@ -93,7 +93,7 @@ func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) e
}
func (c *CacheFile) SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger) {
saveKey := saveRDRCCacheKey{transportName, qName, qType}
saveKey := saveCacheKey{transportName, qName, qType}
c.saveRDRCAccess.Lock()
c.saveRDRC[saveKey] = true
c.saveRDRCAccess.Unlock()

View File

@@ -120,6 +120,24 @@ var OptionLegacyDNSRuleStrategy = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-dns-rule-action-strategy-to-rule-items",
}
var OptionIndependentDNSCache = Note{
Name: "independent-dns-cache",
Description: "`independent_cache` DNS option",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "INDEPENDENT_DNS_CACHE",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-independent-dns-cache",
}
var OptionStoreRDRC = Note{
Name: "store-rdrc",
Description: "`store_rdrc` cache file option",
DeprecatedVersion: "1.14.0",
ScheduledVersion: "1.16.0",
EnvName: "STORE_RDRC",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-store-rdrc",
}
var Options = []Note{
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
@@ -128,4 +146,6 @@ var Options = []Note{
OptionRuleSetIPCIDRAcceptEmpty,
OptionLegacyDNSAddressFilter,
OptionLegacyDNSRuleStrategy,
OptionIndependentDNSCache,
OptionStoreRDRC,
}

View File

@@ -769,7 +769,7 @@ func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler)
for {
event, recvErr := stream.Recv()
if recvErr != nil {
if status.Code(recvErr) == codes.NotFound {
if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable {
return nil
}
recvErr = E.Cause(recvErr, "tailscale status recv")

62
go.mod
View File

@@ -31,8 +31,8 @@ require (
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc
github.com/sagernet/cronet-go v0.0.0-20260410123506-30af64155529
github.com/sagernet/cronet-go/all v0.0.0-20260410123506-30af64155529
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
@@ -109,35 +109,35 @@ require (
github.com/prometheus-community/pro-bing v0.4.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 // indirect
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260410122836-cce5e03076fc // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect

124
go.sum
View File

@@ -168,68 +168,68 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc h1:YK7PwJT0irRAEui9ASdXSxcE2BOVQipWMF/A1Ogt+7c=
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc h1:EJPHOqk23IuBsTjXK9OXqkNxPbKOBWKRmviQoCcriAs=
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc/go.mod h1:8aty0RW96DrJSMWXO6bRPMBJEjuqq5JWiOIi4bCRzFA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9 h1:Y7lWrZwEhC/HX8Pb5C92CrQihuaE7hrHmWB2ykst3iQ=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:3Ggy5wiyjA6t+aVVPnXlSEIVj9zkxd4ybH3NsvsNefs=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:DuFTCnZloblY+7olXiZoRdueWfxi34EV5UheTFKM2rA=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:x/6T2gjpLw9yNdCVR6xBlzMUzED9fxNFNt6U6A6SOh8=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Lx9PExM70rg8aNxPm0JPeSr5SWC3yFiCz4wIq86ugx8=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:BTEpw7/vKR9BNBsHebfpiGHDCPpjVJ3vLIbHNU3VUfM=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:hdEph9nQXRnKwc/lIDwo15rmzbC6znXF5jJWHPN1Fiw=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Iq++oYV7dtRJHTpu8yclHJdn+1oj2t1e84/YpdXYWW8=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9 h1:Y43fuLL8cgwRHpEKwxh0O3vYp7g/SZGvbkJj3cQ6USA=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:bX2GJmF0VCC+tBrVAa49YEsmJ4A9dLmwoA6DJUxRtCY=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:gQTR/2azUCInE0r3kmesZT9xu+x801+BmtDY0d0Tw9Y=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9 h1:X4mP3jlYvxgrKpZLOKMmc/O8T5/zP83/23pgfQOc3tY=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:c6xj2nXr/65EDiRFddUKQIBQ/b/lAPoH8WFYlgadaPc=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:ahbl7yjOvGVVNUwk9TcQk+xejVfoYAYFRlhWnby0/YM=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9 h1:JC5Zv5+J85da6g5G56VhdaK53fmo6Os2q/wWi5QlxOw=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9 h1:4bt7Go588BoM4VjNYMxx0MrvbwlFQn3DdRDCM7BmkRo=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:E1z0BeLUh8EZfCjIyS9BrfCocZrt+0KPS0bzop3Sxf4=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9 h1:d8ejxRHO7Vi9JqR/6DxR7RyI/swA2JfDWATR4T7otBw=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9 h1:iUDVEVu3RxL5ArPIY72BesbuX5zQ1la/ZFwKpQcGc5c=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9 h1:xB6ikOC/R3n3hjy68EJ0sbZhH4vwEhd6JM9jZ1U2SVY=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9 h1:mBOuLCPOOMMq8N1+dUM5FqZclqga1+u6fAbPqQcbIhc=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9 h1:cwPyDfj+ZNFE7kvcWbayQJyeC/KQA16HTXOxgHphL0w=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Zk9zG8kt3mXAboclUXQlvvxKQuhnI8u5NdDEl8uotNY=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:Lu05srGqddQRMnl1MZtGAReln2yJljeGx9b1IadlMJ8=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9 h1:Tk9bDywUmOtc0iMjjCVIwMlAQNsxCy+bK+bTNA0OaBE=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:tQqDQw3tEHdQpt7NTdAwF3UvZ3CjNIj/IJKMRFmm388=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9 h1:biUIbI2YxUrcQikEfS/bwPA8NsHp/WO+VZUG4morUmE=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/cronet-go v0.0.0-20260410123506-30af64155529 h1:tG9OIgS2yHlPAV3JdeUxsRB5v/G+SLKJpxqzpp6k8Oo=
github.com/sagernet/cronet-go v0.0.0-20260410123506-30af64155529/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
github.com/sagernet/cronet-go/all v0.0.0-20260410123506-30af64155529 h1:lVtf0GEzHsM7kB8pOgLMXDtiHRdLd9YHJxiFJYA+UtY=
github.com/sagernet/cronet-go/all v0.0.0-20260410123506-30af64155529/go.mod h1:9ojC4hR6aYIFjEJYlWDdVsirXLugrOW+ww6n4qoU4rk=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260410122836-cce5e03076fc h1:kIrWkB7LXP+ff8d93ZgRJxY3CMMDCQ3UpJXXCtCN7g4=
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260410122836-cce5e03076fc h1:zlNxXRb10o8dBzKyoKQjECHVFJKK/E6WZELxAbO+1xs=
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260410122836-cce5e03076fc h1:bVQhCxXgckcDAgbFewjgHrqaANbnjzl7LdqOWnjlNeI=
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260410122836-cce5e03076fc/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260410122836-cce5e03076fc h1:ANWZXcqJEK6xYPSRkaUvHJHfCzcTVEYyfkzztE9d/e0=
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260410122836-cce5e03076fc h1:OkwOWNnQKFOZPhCH3JTJBy2nGVLDMiQsJQi7//uPOaI=
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260410122836-cce5e03076fc h1:ZtCNLhBZSCsHcFQjTA9UiZBL8e2a//HJITCCsn/m8/g=
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260410122836-cce5e03076fc h1:E2gNT2Hy363eGs72OXEgav63RbvNF7Ptm/71OOG5Ibs=
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260410122836-cce5e03076fc/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260410122836-cce5e03076fc h1:aZaOSYcELkqhtEnRzeKSC65TzctvdUQjXTSr3CaEhfo=
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260410122836-cce5e03076fc h1:vnwtUyYifqFN9mywy+aBFUFpQkcy2IfioGuGvsNDhf0=
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260410122836-cce5e03076fc/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260410122836-cce5e03076fc h1:w1CclLzyKS38gzD+q3jm60Hr86BhAQ1xiDZ0n8m0nY0=
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260410122836-cce5e03076fc h1:Q/gk7S02kQ0bvUDFrNwpKJKVJdjl7kJWzWZKljgi+5w=
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260410122836-cce5e03076fc h1:Ai172rUVUzkuHFkNfLldvRG2dI1ClGScI7FtUZWLots=
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260410122836-cce5e03076fc h1:1o6xtlAe1HzwESswMnQoHu1r1n+cLdGAFfPpX+bHO8M=
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260410122836-cce5e03076fc h1:Sti8C+dQouIBjURJwmEMKztLHSYWsCen40gwgHgwzSM=
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260410122836-cce5e03076fc/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260410122836-cce5e03076fc h1:KdClWyb0mkhRSbMgwz14n6Ibo5EKZkrgTWFYIm9BI+0=
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260410122836-cce5e03076fc h1:cO3NyWvhWEEzoSgT8JZwCsR/3P8exvfGVm/K4ZEyNEQ=
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260410122836-cce5e03076fc h1:ZZ/TroXsTbefE7RaZf6TLYBhiniSQkiXZNU75yizot4=
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260410122836-cce5e03076fc h1:A9IcfsvbWk2MEnT+DAtD72FOA6X9AdZ/UohFmF651XE=
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260410122836-cce5e03076fc h1:/5x0yFE0Qs8jX5/H8cnZwaMiu6ZxCVHvHJQa2GIcw80=
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260410122836-cce5e03076fc h1:9+/b5fc+/Nk8J+UnxSC9oQOUFJIauxv1v1M/PxCcEeo=
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260410122836-cce5e03076fc/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260410122836-cce5e03076fc h1:7T4XbPCZt4ZNVtovgfVKq+cgZMRl6O2nmBVr9dprSR8=
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260410122836-cce5e03076fc/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260410122836-cce5e03076fc h1:75W3nWXJNuGFt57/Gdyl5b2kO1o0lLX+7BNyI33H4eE=
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260410122836-cce5e03076fc h1:qFIm4mvWQjMoTA5qtc6rwzlnfNOVvgnGnyzDkuxHx/w=
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260410122836-cce5e03076fc h1:jN/F8+FuGClq/VniawnvnEfjFJ6r8CC6e1ExJnnz6+w=
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260410122836-cce5e03076fc/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260410122836-cce5e03076fc h1:P9EjLKcKbnb+nBBd2xu0dmLX+FwZxr8Z09MgiFZDGDI=
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260410122836-cce5e03076fc/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260410122836-cce5e03076fc h1:k8QhIShmy781KH5arAeYuLx/aJhIXsOJSy6NLsndtWA=
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260410122836-cce5e03076fc h1:xSo4RX7nrAcZangMn4YZ/23AReudgXUKcr+cq8xtt1A=
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260410122836-cce5e03076fc/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260410122836-cce5e03076fc h1:zCCXtvnPSX5Jmv9wMCh/kXAtOMmqd/HuRsM/m60CoaI=
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260410122836-cce5e03076fc h1:5f1Gl0sqSZvt8wTcr0MXpn5L1qExJeatcx+1fPoYsus=
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260410122836-cce5e03076fc/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
github.com/sagernet/gomobile v0.1.12 h1:XwzjZaclFF96deLqwAgK8gU3w0M2A8qxgDmhV+A0wjg=

View File

@@ -52,9 +52,32 @@ type DNSClientOptions struct {
DisableExpire bool `json:"disable_expire,omitempty"`
IndependentCache bool `json:"independent_cache,omitempty"`
CacheCapacity uint32 `json:"cache_capacity,omitempty"`
Optimistic *OptimisticDNSOptions `json:"optimistic,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type _OptimisticDNSOptions struct {
Enabled bool `json:"enabled,omitempty"`
Timeout badoption.Duration `json:"timeout,omitempty"`
}
type OptimisticDNSOptions _OptimisticDNSOptions
func (o OptimisticDNSOptions) MarshalJSON() ([]byte, error) {
if o.Timeout == 0 {
return json.Marshal(o.Enabled)
}
return json.Marshal((_OptimisticDNSOptions)(o))
}
func (o *OptimisticDNSOptions) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, &o.Enabled)
if err == nil {
return nil
}
return json.UnmarshalDisallowUnknownFields(bytes, (*_OptimisticDNSOptions)(o))
}
type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool)
}

View File

@@ -16,6 +16,7 @@ type CacheFileOptions struct {
StoreFakeIP bool `json:"store_fakeip,omitempty"`
StoreRDRC bool `json:"store_rdrc,omitempty"`
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
StoreDNS bool `json:"store_dns,omitempty"`
}
type ClashAPIOptions struct {

View File

@@ -93,11 +93,12 @@ type DialerOptions struct {
}
type _DomainResolveOptions struct {
Server string `json:"server"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Server string `json:"server"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type DomainResolveOptions _DomainResolveOptions
@@ -107,6 +108,7 @@ func (o DomainResolveOptions) MarshalJSON() ([]byte, error) {
return []byte("{}"), nil
} else if o.Strategy == DomainStrategy(C.DomainStrategyAsIS) &&
!o.DisableCache &&
!o.DisableOptimisticCache &&
o.RewriteTTL == nil &&
o.ClientSubnet == nil {
return json.Marshal(o.Server)

View File

@@ -201,18 +201,20 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
}
type DNSRouteActionOptions struct {
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type _DNSRouteOptionsActionOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions
@@ -321,11 +323,12 @@ type RouteActionSniff struct {
}
type RouteActionResolve struct {
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
Server string `json:"server,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableOptimisticCache bool `json:"disable_optimistic_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"`
}
type DNSRouteActionPredefined struct {

View File

@@ -78,10 +78,11 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, options
RoutingMark: uint32(options.DefaultMark),
DomainResolver: defaultDomainResolver.Server,
DomainResolveOptions: adapter.DNSQueryOptions{
Strategy: C.DomainStrategy(defaultDomainResolver.Strategy),
DisableCache: defaultDomainResolver.DisableCache,
RewriteTTL: defaultDomainResolver.RewriteTTL,
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}),
Strategy: C.DomainStrategy(defaultDomainResolver.Strategy),
DisableCache: defaultDomainResolver.DisableCache,
DisableOptimisticCache: defaultDomainResolver.DisableOptimisticCache,
RewriteTTL: defaultDomainResolver.RewriteTTL,
ClientSubnet: defaultDomainResolver.ClientSubnet.Build(netip.Prefix{}),
},
NetworkStrategy: (*C.NetworkStrategy)(options.DefaultNetworkStrategy),
NetworkType: common.Map(options.DefaultNetworkType, option.InterfaceType.Build),

View File

@@ -219,7 +219,6 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
/*if deadline.NeedAdditionalReadDeadline(conn) {
conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn))
}*/
selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn)
if err != nil {
return err
@@ -816,11 +815,12 @@ func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundCon
}
}
addresses, err := r.dns.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, adapter.DNSQueryOptions{
Transport: transport,
Strategy: action.Strategy,
DisableCache: action.DisableCache,
RewriteTTL: action.RewriteTTL,
ClientSubnet: action.ClientSubnet,
Transport: transport,
Strategy: action.Strategy,
DisableCache: action.DisableCache,
DisableOptimisticCache: action.DisableOptimisticCache,
RewriteTTL: action.RewriteTTL,
ClientSubnet: action.ClientSubnet,
})
if err != nil {
return err

View File

@@ -107,11 +107,12 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
return sniffAction, sniffAction.build()
case C.RuleActionTypeResolve:
return &RuleActionResolve{
Server: action.ResolveOptions.Server,
Strategy: C.DomainStrategy(action.ResolveOptions.Strategy),
DisableCache: action.ResolveOptions.DisableCache,
RewriteTTL: action.ResolveOptions.RewriteTTL,
ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}),
Server: action.ResolveOptions.Server,
Strategy: C.DomainStrategy(action.ResolveOptions.Strategy),
DisableCache: action.ResolveOptions.DisableCache,
DisableOptimisticCache: action.ResolveOptions.DisableOptimisticCache,
RewriteTTL: action.ResolveOptions.RewriteTTL,
ClientSubnet: action.ResolveOptions.ClientSubnet.Build(netip.Prefix{}),
}, nil
default:
panic(F.ToString("unknown rule action: ", action.Action))
@@ -126,30 +127,33 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction)
return &RuleActionDNSRoute{
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)),
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
DisableCache: action.RouteOptions.DisableCache,
DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
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)),
Strategy: C.DomainStrategy(action.RouteOptions.Strategy),
DisableCache: action.RouteOptions.DisableCache,
DisableOptimisticCache: action.RouteOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)),
},
}
case C.RuleActionTypeRespond:
return &RuleActionRespond{}
case C.RuleActionTypeRouteOptions:
return &RuleActionDNSRouteOptions{
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
DisableCache: action.RouteOptionsOptions.DisableCache,
RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
Strategy: C.DomainStrategy(action.RouteOptionsOptions.Strategy),
DisableCache: action.RouteOptionsOptions.DisableCache,
DisableOptimisticCache: action.RouteOptionsOptions.DisableOptimisticCache,
RewriteTTL: action.RouteOptionsOptions.RewriteTTL,
ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)),
}
case C.RuleActionTypeReject:
return &RuleActionReject{
@@ -310,6 +314,9 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou
if options.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
if options.DisableOptimisticCache {
descriptions = append(descriptions, "disable-optimistic-cache")
}
if options.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *options.RewriteTTL))
}
@@ -320,10 +327,11 @@ func formatDNSRouteAction(action string, server string, options RuleActionDNSRou
}
type RuleActionDNSRouteOptions struct {
Strategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Strategy C.DomainStrategy
DisableCache bool
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func (r *RuleActionDNSRouteOptions) Type() string {
@@ -335,6 +343,9 @@ func (r *RuleActionDNSRouteOptions) String() string {
if r.DisableCache {
descriptions = append(descriptions, "disable-cache")
}
if r.DisableOptimisticCache {
descriptions = append(descriptions, "disable-optimistic-cache")
}
if r.RewriteTTL != nil {
descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL))
}
@@ -510,11 +521,12 @@ func (r *RuleActionSniff) String() string {
}
type RuleActionResolve struct {
Server string
Strategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
Server string
Strategy C.DomainStrategy
DisableCache bool
DisableOptimisticCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func (r *RuleActionResolve) Type() string {
@@ -532,6 +544,9 @@ func (r *RuleActionResolve) String() string {
if r.DisableCache {
options = append(options, "disable_cache")
}
if r.DisableOptimisticCache {
options = append(options, "disable_optimistic_cache")
}
if r.RewriteTTL != nil {
options = append(options, F.ToString("rewrite_ttl=", *r.RewriteTTL))
}