Compare commits

...

34 Commits

Author SHA1 Message Date
dependabot[bot]
9e638efe78 build(deps): bump google.golang.org/grpc from 1.77.0 to 1.79.3 in /test
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.77.0 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.77.0...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-11 04:13:44 +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
245 changed files with 21850 additions and 2366 deletions

View File

@@ -1 +1 @@
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
335e5bef5d88fc4474c9a70b865561f45a67de83

View File

@@ -0,0 +1,21 @@
package certificate
type Adapter struct {
providerType string
providerTag string
}
func NewAdapter(providerType string, providerTag string) Adapter {
return Adapter{
providerType: providerType,
providerTag: providerTag,
}
}
func (a *Adapter) Type() string {
return a.providerType
}
func (a *Adapter) Tag() string {
return a.providerTag
}

View File

@@ -0,0 +1,158 @@
package certificate
import (
"context"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
var _ adapter.CertificateProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.CertificateProviderRegistry
access sync.Mutex
started bool
stage adapter.StartStage
providers []adapter.CertificateProviderService
providerByTag map[string]adapter.CertificateProviderService
}
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.CertificateProviderService),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err := adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
providers := m.providers
m.providers = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, provider := range providers {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
m.logger.Trace("close ", name)
startTime := time.Now()
monitor.Start("close ", name)
err = E.Append(err, provider.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
return err
}
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
m.access.Lock()
defer m.access.Unlock()
return m.providers
}
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
m.access.Lock()
provider, found := m.providerByTag[tag]
m.access.Unlock()
return provider, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == provider
})
if index == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return provider.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
for _, stage := range adapter.ListStartStages {
m.logger.Trace(stage, " ", name)
startTime := time.Now()
err = adapter.LegacyStart(provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
}
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = existsProvider.Close()
if err != nil {
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
return it == existsProvider
})
if existsIndex == -1 {
panic("invalid certificate provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
return nil
}

View File

@@ -0,0 +1,72 @@
package certificate
import (
"context"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error)
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
registry.register(providerType, func() any {
return new(Options)
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
var options *Options
if rawOptions != nil {
options = rawOptions.(*Options)
}
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
})
}
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
type (
optionsConstructorFunc func() any
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error)
)
type Registry struct {
access sync.Mutex
optionsType map[string]optionsConstructorFunc
constructor map[string]constructorFunc
}
func NewRegistry() *Registry {
return &Registry{
optionsType: make(map[string]optionsConstructorFunc),
constructor: make(map[string]constructorFunc),
}
}
func (m *Registry) CreateOptions(providerType string) (any, bool) {
m.access.Lock()
defer m.access.Unlock()
optionsConstructor, loaded := m.optionsType[providerType]
if !loaded {
return nil, false
}
return optionsConstructor(), true
}
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
m.access.Lock()
defer m.access.Unlock()
constructor, loaded := m.constructor[providerType]
if !loaded {
return nil, E.New("certificate provider type not found: " + providerType)
}
return constructor(ctx, logger, tag, options)
}
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
m.access.Lock()
defer m.access.Unlock()
m.optionsType[providerType] = optionsConstructor
m.constructor[providerType] = constructor
}

View File

@@ -0,0 +1,38 @@
package adapter
import (
"context"
"crypto/tls"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type CertificateProvider interface {
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
}
type ACMECertificateProvider interface {
CertificateProvider
GetACMENextProtos() []string
}
type CertificateProviderService interface {
Lifecycle
Type() string
Tag() string
CertificateProvider
}
type CertificateProviderRegistry interface {
option.CertificateProviderOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
}
type CertificateProviderManager interface {
Lifecycle
CertificateProviders() []CertificateProviderService
Get(tag string) (CertificateProviderService, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
}

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"
@@ -25,18 +26,19 @@ type DNSRouter interface {
type DNSClient interface {
Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
ClearCache()
}
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
@@ -72,11 +82,6 @@ type DNSTransport interface {
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
}
type LegacyDNSTransport interface {
LegacyStrategy() C.DomainStrategy
LegacyClientSubnet() netip.Prefix
}
type DNSTransportRegistry interface {
option.DNSTransportOptionsRegistry
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)

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

View File

@@ -2,6 +2,7 @@ package adapter
import (
"context"
"net"
"net/netip"
"time"
@@ -9,6 +10,8 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
"github.com/miekg/dns"
)
type Inbound interface {
@@ -78,12 +81,16 @@ type InboundContext struct {
FallbackNetworkType []C.InterfaceType
FallbackDelay time.Duration
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
QueryType uint16
FakeIP bool
DestinationAddresses []netip.Addr
DNSResponse *dns.Msg
DestinationAddressMatchFromResponse bool
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *ConnectionOwner
SourceMACAddress net.HardwareAddr
SourceHostname string
QueryType uint16
FakeIP bool
// rule cache
@@ -112,6 +119,51 @@ func (c *InboundContext) ResetRuleMatchCache() {
c.DidMatch = false
}
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
return DNSResponseAddresses(c.DNSResponse)
}
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawRecord := range response.Answer {
switch record := rawRecord.(type) {
case *dns.A:
addr := M.AddrFromIP(record.A)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.AAAA:
addr := M.AddrFromIP(record.AAAA)
if addr.IsValid() {
addresses = append(addresses, addr)
}
case *dns.HTTPS:
for _, value := range record.SVCB.Value {
switch hint := value.(type) {
case *dns.SVCBIPv4Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip).Unmap()
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
case *dns.SVCBIPv6Hint:
for _, ip := range hint.Hint {
addr := M.AddrFromIP(ip)
if addr.IsValid() {
addresses = append(addresses, addr)
}
}
}
}
}
}
return addresses
}
type inboundContextKey struct{}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {

45
adapter/inbound_test.go Normal file
View File

@@ -0,0 +1,45 @@
package adapter
import (
"net"
"net/netip"
"testing"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
t.Parallel()
ipv4Hint := net.ParseIP("1.1.1.1")
require.NotNil(t, ipv4Hint)
response := &dns.Msg{
MsgHdr: dns.MsgHdr{
Response: true,
Rcode: dns.RcodeSuccess,
},
Answer: []dns.RR{
&dns.HTTPS{
SVCB: dns.SVCB{
Hdr: dns.RR_Header{
Name: dns.Fqdn("example.com"),
Rrtype: dns.TypeHTTPS,
Class: dns.ClassINET,
Ttl: 60,
},
Priority: 1,
Target: ".",
Value: []dns.SVCBKeyValue{
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
},
},
},
},
}
addresses := DNSResponseAddresses(response)
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
require.True(t, addresses[0].Is4())
}

23
adapter/neighbor.go Normal file
View File

@@ -0,0 +1,23 @@
package adapter
import (
"net"
"net/netip"
)
type NeighborEntry struct {
Address netip.Addr
MACAddress net.HardwareAddr
Hostname string
}
type NeighborResolver interface {
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
LookupHostname(address netip.Addr) (string, bool)
Start() error
Close() error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries []NeighborEntry)
}

View File

@@ -36,6 +36,10 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {

View File

@@ -26,6 +26,8 @@ type Router interface {
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
NeedFindProcess() bool
NeedFindNeighbor() bool
NeighborResolver() NeighborResolver
AppendTracker(tracker ConnectionTracker)
ResetNetwork()
}
@@ -64,10 +66,16 @@ type RuleSet interface {
type RuleSetUpdateCallback func(it RuleSet)
type DNSRuleSetUpdateValidator interface {
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
}
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
ContainsDNSQueryTypeRule bool
}
type HTTPStartContext struct {
ctx context.Context

View File

@@ -2,6 +2,8 @@ package adapter
import (
C "github.com/sagernet/sing-box/constant"
"github.com/miekg/dns"
)
type HeadlessRule interface {
@@ -18,8 +20,9 @@ type Rule interface {
type DNSRule interface {
Rule
LegacyPreMatch(metadata *InboundContext) bool
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
}
type RuleAction interface {
@@ -29,7 +32,7 @@ type RuleAction interface {
func IsFinalAction(action RuleAction) bool {
switch action.Type() {
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
return false
default:
return true

49
adapter/tailscale.go Normal file
View File

@@ -0,0 +1,49 @@
package adapter
import "context"
type TailscaleEndpoint interface {
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
}
type TailscalePingResult struct {
LatencyMs float64
IsDirect bool
Endpoint string
DERPRegionID int32
DERPRegionCode string
Error string
}
type TailscaleEndpointStatus struct {
BackendState string
AuthURL string
NetworkName string
MagicDNSSuffix string
Self *TailscalePeer
UserGroups []*TailscaleUserGroup
}
type TailscaleUserGroup struct {
UserID int64
LoginName string
DisplayName string
ProfilePicURL string
Peers []*TailscalePeer
}
type TailscalePeer struct {
HostName string
DNSName string
OS string
TailscaleIPs []string
Online bool
ExitNode bool
ExitNodeOption bool
Active bool
RxBytes int64
TxBytes int64
UserID int64
KeyExpiry int64
}

133
box.go
View File

@@ -9,6 +9,7 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
@@ -36,20 +37,21 @@ import (
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
createdAt time.Time
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
endpoint *endpoint.Manager
inbound *inbound.Manager
outbound *outbound.Manager
service *boxService.Manager
certificateProvider *boxCertificate.Manager
dnsTransport *dns.TransportManager
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
internalService []adapter.LifecycleService
done chan struct{}
}
type Options struct {
@@ -65,6 +67,7 @@ func Context(
endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
certificateProviderRegistry adapter.CertificateProviderRegistry,
) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -89,6 +92,10 @@ func Context(
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
}
return ctx
}
@@ -105,6 +112,7 @@ func New(options Options) (*Box, error) {
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context")
@@ -121,6 +129,9 @@ func New(options Options) (*Box, error) {
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
if certificateProviderRegistry == nil {
return nil, E.New("missing certificate provider registry in context")
}
ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -178,13 +189,19 @@ func New(options Options) (*Box, error) {
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
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)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
@@ -271,6 +288,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize inbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
for i, outboundOptions := range options.Outbounds {
var tag string
if outboundOptions.Tag != "" {
@@ -297,22 +332,22 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]")
}
}
for i, serviceOptions := range options.Services {
for i, certificateProviderOptions := range options.CertificateProviders {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
if certificateProviderOptions.Tag != "" {
tag = certificateProviderOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
err = certificateProviderManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
certificateProviderOptions.Type,
certificateProviderOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
}
}
outboundManager.Initialize(func() (adapter.Outbound, error) {
@@ -340,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)
}
@@ -383,20 +418,21 @@ func New(options Options) (*Box, error) {
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
outbound: outboundManager,
dnsTransport: dnsTransportManager,
service: serviceManager,
certificateProvider: certificateProviderManager,
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
}, nil
}
@@ -450,11 +486,11 @@ func (s *Box) preStart() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection, s.router, s.dnsRouter)
if err != nil {
return err
}
@@ -470,11 +506,19 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -482,7 +526,7 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
@@ -506,8 +550,9 @@ func (s *Box) Close() error {
service adapter.Lifecycle
}{
{"service", s.service},
{"endpoint", s.endpoint},
{"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"endpoint", s.endpoint},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},

View File

@@ -82,6 +82,11 @@ func compileRuleSet(sourcePath string) error {
}
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return len(rule.PackageNameRegex) > 0
}) {
version = C.RuleSetVersion4
}
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
len(rule.DefaultInterfaceAddress) > 0

View File

@@ -0,0 +1,121 @@
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var (
commandNetworkQualityFlagConfigURL string
commandNetworkQualityFlagSerial bool
commandNetworkQualityFlagMaxRuntime int
commandNetworkQualityFlagHTTP3 bool
)
var commandNetworkQuality = &cobra.Command{
Use: "networkquality",
Short: "Run a network quality test",
Run: func(cmd *cobra.Command, args []string) {
err := runNetworkQuality()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandNetworkQuality.Flags().StringVar(
&commandNetworkQualityFlagConfigURL,
"config-url", "",
"Network quality test config URL (default: Apple mensura)",
)
commandNetworkQuality.Flags().BoolVar(
&commandNetworkQualityFlagSerial,
"serial", false,
"Run download and upload tests sequentially instead of in parallel",
)
commandNetworkQuality.Flags().IntVar(
&commandNetworkQualityFlagMaxRuntime,
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
"Network quality maximum runtime in seconds",
)
commandNetworkQuality.Flags().BoolVar(
&commandNetworkQualityFlagHTTP3,
"http3", false,
"Use HTTP/3 (QUIC) for measurement traffic",
)
commandTools.AddCommand(commandNetworkQuality)
}
func runNetworkQuality() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
httpClient := networkquality.NewHTTPClient(dialer)
defer httpClient.CloseIdleConnections()
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
result, err := networkquality.Run(networkquality.Options{
ConfigURL: commandNetworkQualityFlagConfigURL,
HTTPClient: httpClient,
NewMeasurementClient: measurementClientFactory,
Serial: commandNetworkQualityFlagSerial,
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
Context: globalCtx,
OnProgress: func(p networkquality.Progress) {
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
return
}
switch networkquality.Phase(p.Phase) {
case networkquality.PhaseIdle:
if p.IdleLatencyMs > 0 {
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
} else {
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
}
case networkquality.PhaseDownload:
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
case networkquality.PhaseUpload:
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
}
},
})
if err != nil {
return err
}
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
return nil
}

View File

@@ -0,0 +1,79 @@
package main
import (
"fmt"
"os"
"github.com/sagernet/sing-box/common/stun"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"
)
var commandSTUNFlagServer string
var commandSTUN = &cobra.Command{
Use: "stun",
Short: "Run a STUN test",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
err := runSTUN()
if err != nil {
log.Fatal(err)
}
},
}
func init() {
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
commandTools.AddCommand(commandSTUN)
}
func runSTUN() error {
instance, err := createPreStartedClient()
if err != nil {
return err
}
defer instance.Close()
dialer, err := createDialer(instance, commandToolsFlagOutbound)
if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
result, err := stun.Run(stun.Options{
Server: commandSTUNFlagServer,
Dialer: dialer,
Context: globalCtx,
OnProgress: func(p stun.Progress) {
switch p.Phase {
case stun.PhaseBinding:
if p.ExternalAddr != "" {
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
} else {
fmt.Fprint(os.Stderr, "\rSending binding request...")
}
case stun.PhaseNATMapping:
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
case stun.PhaseNATFiltering:
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
}
},
})
if err != nil {
return err
}
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
if result.NATTypeSupported {
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
} else {
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
}
return nil
}

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

@@ -0,0 +1,142 @@
package networkquality
import (
"context"
"fmt"
"net"
"net/http"
"strings"
C "github.com/sagernet/sing-box/constant"
sBufio "github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func FormatBitrate(bps int64) string {
switch {
case bps >= 1_000_000_000:
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
case bps >= 1_000_000:
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
case bps >= 1_000:
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
default:
return fmt.Sprintf("%d bps", bps)
}
}
func NewHTTPClient(dialer N.Dialer) *http.Client {
transport := &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: C.TCPTimeout,
}
if dialer != nil {
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}
}
return &http.Client{Transport: transport}
}
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
if client == nil {
return nil, E.New("http client is nil")
}
if client.Transport == nil {
return http.DefaultTransport.(*http.Transport).Clone(), nil
}
transport, ok := client.Transport.(*http.Transport)
if !ok {
return nil, E.New("http client transport must be *http.Transport")
}
return transport.Clone(), nil
}
func newMeasurementClient(
baseClient *http.Client,
connectEndpoint string,
singleConnection bool,
disableKeepAlives bool,
readCounters []N.CountFunc,
writeCounters []N.CountFunc,
) (*http.Client, error) {
transport, err := baseTransportFromClient(baseClient)
if err != nil {
return nil, err
}
transport.DisableCompression = true
transport.DisableKeepAlives = disableKeepAlives
if singleConnection {
transport.MaxConnsPerHost = 1
transport.MaxIdleConnsPerHost = 1
transport.MaxIdleConns = 1
}
baseDialContext := transport.DialContext
if baseDialContext == nil {
dialer := &net.Dialer{}
baseDialContext = dialer.DialContext
}
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
dialAddr := addr
if connectEndpoint != "" {
dialAddr = rewriteDialAddress(addr, connectEndpoint)
}
conn, dialErr := baseDialContext(ctx, network, dialAddr)
if dialErr != nil {
return nil, dialErr
}
if len(readCounters) > 0 || len(writeCounters) > 0 {
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
}
return conn, nil
}
return &http.Client{
Transport: transport,
CheckRedirect: baseClient.CheckRedirect,
Jar: baseClient.Jar,
Timeout: baseClient.Timeout,
}, nil
}
type MeasurementClientFactory func(
connectEndpoint string,
singleConnection bool,
disableKeepAlives bool,
readCounters []N.CountFunc,
writeCounters []N.CountFunc,
) (*http.Client, error)
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
}
}
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
if !useHTTP3 {
return nil, nil
}
return NewHTTP3MeasurementClientFactory(dialer)
}
func rewriteDialAddress(addr string, connectEndpoint string) string {
connectEndpoint = strings.TrimSpace(connectEndpoint)
host, port, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
if err == nil {
host = endpointHost
if endpointPort != "" {
port = endpointPort
}
} else if connectEndpoint != "" {
host = connectEndpoint
}
return net.JoinHostPort(host, port)
}

View File

@@ -0,0 +1,55 @@
//go:build with_quic
package networkquality
import (
"context"
"crypto/tls"
"net"
"net/http"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
sBufio "github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
// singleConnection and disableKeepAlives are not applied:
// HTTP/3 multiplexes streams over a single QUIC connection by default.
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
transport := &http3.Transport{
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
dialAddr := addr
if connectEndpoint != "" {
dialAddr = rewriteDialAddress(addr, connectEndpoint)
}
destination := M.ParseSocksaddr(dialAddr)
var udpConn net.Conn
var dialErr error
if dialer != nil {
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
} else {
var netDialer net.Dialer
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
}
if dialErr != nil {
return nil, dialErr
}
wrappedConn := udpConn
if len(readCounters) > 0 || len(writeCounters) > 0 {
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
}
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
if dialErr != nil {
udpConn.Close()
return nil, dialErr
}
return quicConn, nil
},
}
return &http.Client{Transport: transport}, nil
}, nil
}

View File

@@ -0,0 +1,12 @@
//go:build !with_quic
package networkquality
import (
C "github.com/sagernet/sing-box/constant"
N "github.com/sagernet/sing/common/network"
)
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
return nil, C.ErrQUICNotIncluded
}

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ const (
ruleItemNetworkIsConstrained
ruleItemNetworkInterfaceAddress
ruleItemDefaultInterfaceAddress
ruleItemPackageNameRegex
ruleItemFinal uint8 = 0xFF
)
@@ -215,6 +216,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
rule.ProcessPathRegex, err = readRuleItemString(reader)
case ruleItemPackageName:
rule.PackageName, err = readRuleItemString(reader)
case ruleItemPackageNameRegex:
rule.PackageNameRegex, err = readRuleItemString(reader)
case ruleItemWIFISSID:
rule.WIFISSID, err = readRuleItemString(reader)
case ruleItemWIFIBSSID:
@@ -394,6 +397,15 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
return err
}
}
if len(rule.PackageNameRegex) > 0 {
if generateVersion < C.RuleSetVersion5 {
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
}
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
if err != nil {
return err
}
}
if len(rule.NetworkType) > 0 {
if generateVersion < C.RuleSetVersion3 {
return E.New("`network_type` rule item is only supported in version 3 or later")

612
common/stun/stun.go Normal file
View File

@@ -0,0 +1,612 @@
package stun
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"net"
"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"
)
const (
DefaultServer = "stun.voipgate.com:3478"
magicCookie = 0x2112A442
headerSize = 20
bindingRequest = 0x0001
bindingSuccessResponse = 0x0101
bindingErrorResponse = 0x0111
attrMappedAddress = 0x0001
attrChangeRequest = 0x0003
attrErrorCode = 0x0009
attrXORMappedAddress = 0x0020
attrOtherAddress = 0x802c
familyIPv4 = 0x01
familyIPv6 = 0x02
changeIP = 0x04
changePort = 0x02
defaultRTO = 500 * time.Millisecond
minRTO = 250 * time.Millisecond
maxRetransmit = 2
)
type Phase int32
const (
PhaseBinding Phase = iota
PhaseNATMapping
PhaseNATFiltering
PhaseDone
)
type NATMapping int32
const (
NATMappingUnknown NATMapping = iota
_ // reserved
NATMappingEndpointIndependent
NATMappingAddressDependent
NATMappingAddressAndPortDependent
)
func (m NATMapping) String() string {
switch m {
case NATMappingEndpointIndependent:
return "Endpoint Independent"
case NATMappingAddressDependent:
return "Address Dependent"
case NATMappingAddressAndPortDependent:
return "Address and Port Dependent"
default:
return "Unknown"
}
}
type NATFiltering int32
const (
NATFilteringUnknown NATFiltering = iota
NATFilteringEndpointIndependent
NATFilteringAddressDependent
NATFilteringAddressAndPortDependent
)
func (f NATFiltering) String() string {
switch f {
case NATFilteringEndpointIndependent:
return "Endpoint Independent"
case NATFilteringAddressDependent:
return "Address Dependent"
case NATFilteringAddressAndPortDependent:
return "Address and Port Dependent"
default:
return "Unknown"
}
}
type TransactionID [12]byte
type Options struct {
Server string
Dialer N.Dialer
Context context.Context
OnProgress func(Progress)
}
type Progress struct {
Phase Phase
ExternalAddr string
LatencyMs int32
NATMapping NATMapping
NATFiltering NATFiltering
}
type Result struct {
ExternalAddr string
LatencyMs int32
NATMapping NATMapping
NATFiltering NATFiltering
NATTypeSupported bool
}
type parsedResponse struct {
xorMappedAddr netip.AddrPort
mappedAddr netip.AddrPort
otherAddr netip.AddrPort
}
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
if r.xorMappedAddr.IsValid() {
return r.xorMappedAddr, true
}
if r.mappedAddr.IsValid() {
return r.mappedAddr, true
}
return netip.AddrPort{}, false
}
type stunAttribute struct {
typ uint16
value []byte
}
func newTransactionID() TransactionID {
var id TransactionID
_, _ = rand.Read(id[:])
return id
}
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
attrLen := 0
for _, attr := range attrs {
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
}
buf := make([]byte, headerSize+attrLen)
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
copy(buf[8:20], txID[:])
offset := headerSize
for _, attr := range attrs {
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
}
return buf
}
func changeRequestAttr(flags byte) stunAttribute {
return stunAttribute{
typ: attrChangeRequest,
value: []byte{0, 0, 0, flags},
}
}
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
if len(data) < headerSize {
return nil, E.New("response too short")
}
msgType := binary.BigEndian.Uint16(data[0:2])
if msgType&0xC000 != 0 {
return nil, E.New("invalid STUN message: top 2 bits not zero")
}
cookie := binary.BigEndian.Uint32(data[4:8])
if cookie != magicCookie {
return nil, E.New("invalid magic cookie")
}
var txID TransactionID
copy(txID[:], data[8:20])
if txID != expectedTxID {
return nil, E.New("transaction ID mismatch")
}
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
if msgLen > len(data)-headerSize {
return nil, E.New("message length exceeds data")
}
attrData := data[headerSize : headerSize+msgLen]
if msgType == bindingErrorResponse {
return nil, parseErrorResponse(attrData)
}
if msgType != bindingSuccessResponse {
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
}
resp := &parsedResponse{}
offset := 0
for offset+4 <= len(attrData) {
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
if offset+4+attrLen > len(attrData) {
break
}
attrValue := attrData[offset+4 : offset+4+attrLen]
switch attrType {
case attrXORMappedAddress:
addr, err := parseXORMappedAddress(attrValue, txID)
if err == nil {
resp.xorMappedAddr = addr
}
case attrMappedAddress:
addr, err := parseMappedAddress(attrValue)
if err == nil {
resp.mappedAddr = addr
}
case attrOtherAddress:
addr, err := parseMappedAddress(attrValue)
if err == nil {
resp.otherAddr = addr
}
}
offset += 4 + attrLen + paddingLen(attrLen)
}
return resp, nil
}
func parseErrorResponse(data []byte) error {
offset := 0
for offset+4 <= len(data) {
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
if offset+4+attrLen > len(data) {
break
}
if attrType == attrErrorCode && attrLen >= 4 {
attrValue := data[offset+4 : offset+4+attrLen]
class := int(attrValue[2] & 0x07)
number := int(attrValue[3])
code := class*100 + number
if attrLen > 4 {
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
}
return E.New("STUN error ", code)
}
offset += 4 + attrLen + paddingLen(attrLen)
}
return E.New("STUN error response")
}
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
if len(data) < 4 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
}
family := data[1]
xPort := binary.BigEndian.Uint16(data[2:4])
port := xPort ^ uint16(magicCookie>>16)
switch family {
case familyIPv4:
if len(data) < 8 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
}
var ip [4]byte
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
case familyIPv6:
if len(data) < 20 {
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
}
var ip [16]byte
var xorKey [16]byte
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
copy(xorKey[4:16], txID[:])
for i := range 16 {
ip[i] = data[4+i] ^ xorKey[i]
}
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
default:
return netip.AddrPort{}, E.New("unknown address family: ", family)
}
}
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
if len(data) < 4 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
}
family := data[1]
port := binary.BigEndian.Uint16(data[2:4])
switch family {
case familyIPv4:
if len(data) < 8 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
}
return netip.AddrPortFrom(
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
), nil
case familyIPv6:
if len(data) < 20 {
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
}
var ip [16]byte
copy(ip[:], data[4:20])
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
default:
return netip.AddrPort{}, E.New("unknown address family: ", family)
}
}
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
request := buildBindingRequest(txID, attrs...)
currentRTO := rto
retransmitCount := 0
sendTime := time.Now()
_, err := conn.WriteTo(request, addr)
if err != nil {
return nil, 0, E.Cause(err, "send STUN request")
}
buf := make([]byte, 1024)
for {
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
if err != nil {
return nil, 0, E.Cause(err, "set read deadline")
}
n, _, readErr := conn.ReadFrom(buf)
if readErr != nil {
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
retransmitCount++
currentRTO *= 2
sendTime = time.Now()
_, err = conn.WriteTo(request, addr)
if err != nil {
return nil, 0, E.Cause(err, "retransmit STUN request")
}
continue
}
return nil, 0, E.Cause(readErr, "read STUN response")
}
if n < headerSize || buf[0]&0xC0 != 0 ||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
continue
}
var receivedTxID TransactionID
copy(receivedTxID[:], buf[8:20])
if receivedTxID != txID {
continue
}
latency := time.Since(sendTime)
resp, parseErr := parseResponse(buf[:n], txID)
if parseErr != nil {
return nil, 0, parseErr
}
return resp, latency, nil
}
}
func Run(options Options) (*Result, error) {
ctx := options.Context
if ctx == nil {
ctx = context.Background()
}
server := options.Server
if server == "" {
server = DefaultServer
}
serverSocksaddr := M.ParseSocksaddr(server)
if serverSocksaddr.Port == 0 {
serverSocksaddr.Port = 3478
}
reportProgress := options.OnProgress
if reportProgress == nil {
reportProgress = func(Progress) {}
}
var (
packetConn net.PacketConn
serverAddr net.Addr
err error
)
if options.Dialer != nil {
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
if err != nil {
return nil, E.Cause(err, "create UDP socket")
}
serverAddr = serverSocksaddr
} else {
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
if resolveErr != nil {
return nil, E.Cause(resolveErr, "resolve STUN server")
}
packetConn, err = net.ListenPacket("udp", "")
if err != nil {
return nil, E.Cause(err, "create UDP socket")
}
serverAddr = serverUDPAddr
}
defer func() {
_ = packetConn.Close()
}()
if deadline.NeedAdditionalReadDeadline(packetConn) {
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
}
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
rto := defaultRTO
// Phase 1: Binding
reportProgress(Progress{Phase: PhaseBinding})
txID := newTransactionID()
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
if err != nil {
return nil, E.Cause(err, "binding request")
}
rto = max(minRTO, 3*latency)
externalAddr, ok := resp.externalAddr()
if !ok {
return nil, E.New("no mapped address in response")
}
result := &Result{
ExternalAddr: externalAddr.String(),
LatencyMs: int32(latency.Milliseconds()),
}
reportProgress(Progress{
Phase: PhaseBinding,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
otherAddr := resp.otherAddr
if !otherAddr.IsValid() {
result.NATTypeSupported = false
reportProgress(Progress{
Phase: PhaseDone,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
return result, nil
}
result.NATTypeSupported = true
select {
case <-ctx.Done():
return result, nil
default:
}
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
reportProgress(Progress{
Phase: PhaseNATMapping,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
})
result.NATMapping = detectNATMapping(
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
)
reportProgress(Progress{
Phase: PhaseNATMapping,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
})
select {
case <-ctx.Done():
return result, nil
default:
}
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
reportProgress(Progress{
Phase: PhaseNATFiltering,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
})
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
reportProgress(Progress{
Phase: PhaseDone,
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NATMapping: result.NATMapping,
NATFiltering: result.NATFiltering,
})
return result, nil
}
func detectNATMapping(
conn net.PacketConn,
serverPort uint16,
externalAddr netip.AddrPort,
otherAddr netip.AddrPort,
rto time.Duration,
) NATMapping {
// Mapping Test II: Send to other_ip:server_port
testIIAddr := net.UDPAddrFromAddrPort(
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
)
txID2 := newTransactionID()
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
if err != nil {
return NATMappingUnknown
}
externalAddr2, ok := resp2.externalAddr()
if !ok {
return NATMappingUnknown
}
if externalAddr == externalAddr2 {
return NATMappingEndpointIndependent
}
// Mapping Test III: Send to other_ip:other_port
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
txID3 := newTransactionID()
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
if err != nil {
return NATMappingUnknown
}
externalAddr3, ok := resp3.externalAddr()
if !ok {
return NATMappingUnknown
}
if externalAddr2 == externalAddr3 {
return NATMappingAddressDependent
}
return NATMappingAddressAndPortDependent
}
func detectNATFiltering(
conn net.PacketConn,
serverAddr net.Addr,
rto time.Duration,
) NATFiltering {
// Filtering Test II: Request response from different IP and port
txID := newTransactionID()
_, _, err := roundTrip(conn, serverAddr, txID,
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
if err == nil {
return NATFilteringEndpointIndependent
}
// Filtering Test III: Request response from different port only
txID = newTransactionID()
_, _, err = roundTrip(conn, serverAddr, txID,
[]stunAttribute{changeRequestAttr(changePort)}, rto)
if err == nil {
return NATFilteringAddressDependent
}
return NATFilteringAddressAndPortDependent
}
func paddingLen(n int) int {
if n%4 == 0 {
return 0
}
return 4 - n%4
}

View File

@@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error {
return nil
}
type acmeLogWriter struct {
logger logger.Logger
}
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.logger.Debug(logLine[7:])
default:
w.logger.Debug(logLine)
}
return len(p), nil
}
func (w *acmeLogWriter) Sync() error {
return nil
}
func encoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string
switch options.Provider {
@@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
storage = certmagic.Default.Storage
}
zapLogger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(encoderConfig()),
&acmeLogWriter{logger: logger},
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
&ACMELogWriter{Logger: logger},
zap.DebugLevel,
))
config := &certmagic.Config{
@@ -158,7 +127,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
} else {
tlsConfig = &tls.Config{
GetCertificate: config.GetCertificate,
NextProtos: []string{ACMETLS1Protocol},
NextProtos: []string{C.ACMETLS1Protocol},
}
}
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil

41
common/tls/acme_logger.go Normal file
View File

@@ -0,0 +1,41 @@
package tls
import (
"strings"
"github.com/sagernet/sing/common/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type ACMELogWriter struct {
Logger logger.Logger
}
func (w *ACMELogWriter) Write(p []byte) (n int, err error) {
logLine := strings.ReplaceAll(string(p), " ", ": ")
switch {
case strings.HasPrefix(logLine, "error: "):
w.Logger.Error(logLine[7:])
case strings.HasPrefix(logLine, "warn: "):
w.Logger.Warn(logLine[6:])
case strings.HasPrefix(logLine, "info: "):
w.Logger.Info(logLine[6:])
case strings.HasPrefix(logLine, "debug: "):
w.Logger.Debug(logLine[7:])
default:
w.Logger.Debug(logLine)
}
return len(p), nil
}
func (w *ACMELogWriter) Sync() error {
return nil
}
func ACMEEncoderConfig() zapcore.EncoderConfig {
config := zap.NewProductionEncoderConfig()
config.TimeKey = zapcore.OmitKey
return config
}

View File

@@ -32,6 +32,10 @@ type RealityServerConfig struct {
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
var tlsConfig utls.RealityConfig
if options.CertificateProvider != nil {
return nil, E.New("certificate_provider is unavailable in reality")
}
//nolint:staticcheck
if options.ACME != nil && len(options.ACME.Domain) > 0 {
return nil, E.New("acme is unavailable in reality")
}

View File

@@ -13,19 +13,87 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
)
var errInsecureUnused = E.New("tls: insecure unused")
type managedCertificateProvider interface {
adapter.CertificateProvider
adapter.SimpleLifecycle
}
type sharedCertificateProvider struct {
tag string
manager adapter.CertificateProviderManager
provider adapter.CertificateProviderService
}
func (p *sharedCertificateProvider) Start() error {
provider, found := p.manager.Get(p.tag)
if !found {
return E.New("certificate provider not found: ", p.tag)
}
p.provider = provider
return nil
}
func (p *sharedCertificateProvider) Close() error {
return nil
}
func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *sharedCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
type inlineCertificateProvider struct {
provider adapter.CertificateProviderService
}
func (p *inlineCertificateProvider) Start() error {
for _, stage := range adapter.ListStartStages {
err := adapter.LegacyStart(p.provider, stage)
if err != nil {
return err
}
}
return nil
}
func (p *inlineCertificateProvider) Close() error {
return p.provider.Close()
}
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
return p.provider.GetCertificate(hello)
}
func (p *inlineCertificateProvider) GetACMENextProtos() []string {
return getACMENextProtos(p.provider)
}
func getACMENextProtos(provider adapter.CertificateProvider) []string {
if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME {
return acmeProvider.GetACMENextProtos()
}
return nil
}
type STDServerConfig struct {
access sync.RWMutex
config *tls.Config
logger log.Logger
certificateProvider managedCertificateProvider
acmeService adapter.SimpleLifecycle
certificate []byte
key []byte
@@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
func (c *STDServerConfig) NextProtos() []string {
c.access.RLock()
defer c.access.RUnlock()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
return c.config.NextProtos[1:]
} else {
return c.config.NextProtos
}
return c.config.NextProtos
}
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.access.Lock()
defer c.access.Unlock()
config := c.config.Clone()
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
} else {
config.NextProtos = nextProto
@@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.config = config
}
func (c *STDServerConfig) hasACMEALPN() bool {
if c.acmeService != nil {
return true
}
if c.certificateProvider != nil {
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
return len(acmeProvider.GetACMENextProtos()) > 0
}
}
return false
}
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
return c.config, nil
}
@@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config {
}
func (c *STDServerConfig) Start() error {
if c.acmeService != nil {
return c.acmeService.Start()
} else {
err := c.startWatcher()
if c.certificateProvider != nil {
err := c.certificateProvider.Start()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
return err
}
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
nextProtos := acmeProvider.GetACMENextProtos()
if len(nextProtos) > 0 {
c.access.Lock()
config := c.config.Clone()
mergedNextProtos := append([]string{}, nextProtos...)
for _, nextProto := range config.NextProtos {
if !common.Contains(mergedNextProtos, nextProto) {
mergedNextProtos = append(mergedNextProtos, nextProto)
}
}
config.NextProtos = mergedNextProtos
c.config = config
c.access.Unlock()
}
}
return nil
}
if c.acmeService != nil {
err := c.acmeService.Start()
if err != nil {
return err
}
}
err := c.startWatcher()
if err != nil {
c.logger.Warn("create fsnotify watcher: ", err)
}
return nil
}
func (c *STDServerConfig) startWatcher() error {
@@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
}
func (c *STDServerConfig) Close() error {
if c.acmeService != nil {
return c.acmeService.Close()
}
if c.watcher != nil {
return c.watcher.Close()
}
return nil
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
}
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
//nolint:staticcheck
if options.CertificateProvider != nil && options.ACME != nil {
return nil, E.New("certificate_provider and acme are mutually exclusive")
}
var tlsConfig *tls.Config
var certificateProvider managedCertificateProvider
var acmeService adapter.SimpleLifecycle
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
if options.CertificateProvider != nil {
certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider)
if err != nil {
return nil, err
}
tlsConfig = &tls.Config{
GetCertificate: certificateProvider.GetCertificate,
}
if options.Insecure {
return nil, errInsecureUnused
}
} else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck
deprecated.Report(ctx, deprecated.OptionInlineACME)
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
if err != nil {
@@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
certificate []byte
key []byte
)
if acmeService == nil {
if certificateProvider == nil && acmeService == nil {
if len(options.Certificate) > 0 {
certificate = []byte(strings.Join(options.Certificate, "\n"))
} else if options.CertificatePath != "" {
@@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
serverConfig := &STDServerConfig{
config: tlsConfig,
logger: logger,
certificateProvider: certificateProvider,
acmeService: acmeService,
certificate: certificate,
key: key,
@@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
echKeyPath: echKeyPath,
}
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
serverConfig.access.Lock()
defer serverConfig.access.Unlock()
serverConfig.access.RLock()
defer serverConfig.access.RUnlock()
return serverConfig.config, nil
}
var config ServerConfig = serverConfig
@@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
}
return config, nil
}
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
if options.IsShared() {
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
if manager == nil {
return nil, E.New("missing certificate provider manager in context")
}
return &sharedCertificateProvider{
tag: options.Tag,
manager: manager,
}, nil
}
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
if registry == nil {
return nil, E.New("missing certificate provider registry in context")
}
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
if err != nil {
return nil, E.Cause(err, "create inline certificate provider")
}
return &inlineCertificateProvider{
provider: provider,
}, nil
}

View File

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

View File

@@ -1,36 +1,39 @@
package constant
const (
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeTun = "tun"
TypeRedirect = "redirect"
TypeTProxy = "tproxy"
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
TypeVMess = "vmess"
TypeTrojan = "trojan"
TypeNaive = "naive"
TypeWireGuard = "wireguard"
TypeHysteria = "hysteria"
TypeTor = "tor"
TypeSSH = "ssh"
TypeShadowTLS = "shadowtls"
TypeAnyTLS = "anytls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale"
TypeCloudflared = "cloudflared"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
TypeCCM = "ccm"
TypeOCM = "ocm"
TypeOOMKiller = "oom-killer"
TypeACME = "acme"
TypeCloudflareOriginCA = "cloudflare-origin-ca"
)
const (
@@ -88,6 +91,8 @@ func ProxyDisplayName(proxyType string) string {
return "AnyTLS"
case TypeTailscale:
return "Tailscale"
case TypeCloudflared:
return "Cloudflared"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@@ -23,12 +23,15 @@ const (
RuleSetVersion2
RuleSetVersion3
RuleSetVersion4
RuleSetVersionCurrent = RuleSetVersion4
RuleSetVersion5
RuleSetVersionCurrent = RuleSetVersion5
)
const (
RuleActionTypeRoute = "route"
RuleActionTypeRouteOptions = "route-options"
RuleActionTypeEvaluate = "evaluate"
RuleActionTypeRespond = "respond"
RuleActionTypeDirect = "direct"
RuleActionTypeBypass = "bypass"
RuleActionTypeReject = "reject"

View File

@@ -1,3 +1,3 @@
package tls
package constant
const ACMETLS1Protocol = "acme-tls/1"

View File

@@ -87,12 +87,17 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
}
}
}
if s.oomKiller && C.IsIos {
if s.oomKillerEnabled {
if !common.Any(options.Services, func(it option.Service) bool {
return it.Type == C.TypeOOMKiller
}) {
oomOptions := &option.OOMKillerServiceOptions{
KillerDisabled: s.oomKillerDisabled,
MemoryLimitOverride: s.oomMemoryLimit,
}
options.Services = append(options.Services, option.Service{
Type: C.TypeOOMKiller,
Type: C.TypeOOMKiller,
Options: oomOptions,
})
}
}

View File

@@ -5,5 +5,6 @@ type PlatformHandler interface {
ServiceReload() error
SystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
TriggerNativeCrash() error
WriteDebugMessage(message string)
}

View File

@@ -6,14 +6,20 @@ import (
"runtime"
"sync"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/networkquality"
"github.com/sagernet/sing-box/common/stun"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/service/oomkiller"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
@@ -24,6 +30,8 @@ import (
"github.com/gofrs/uuid/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
@@ -32,10 +40,12 @@ var _ StartedServiceServer = (*StartedService)(nil)
type StartedService struct {
ctx context.Context
// platform adapter.PlatformInterface
handler PlatformHandler
debug bool
logMaxLines int
oomKiller bool
handler PlatformHandler
debug bool
logMaxLines int
oomKillerEnabled bool
oomKillerDisabled bool
oomMemoryLimit uint64
// workingDirectory string
// tempDirectory string
// userID int
@@ -64,10 +74,12 @@ type StartedService struct {
type ServiceOptions struct {
Context context.Context
// Platform adapter.PlatformInterface
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKiller bool
Handler PlatformHandler
Debug bool
LogMaxLines int
OOMKillerEnabled bool
OOMKillerDisabled bool
OOMMemoryLimit uint64
// WorkingDirectory string
// TempDirectory string
// UserID int
@@ -79,10 +91,12 @@ func NewStartedService(options ServiceOptions) *StartedService {
s := &StartedService{
ctx: options.Context,
// platform: options.Platform,
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKiller: options.OOMKiller,
handler: options.Handler,
debug: options.Debug,
logMaxLines: options.LogMaxLines,
oomKillerEnabled: options.OOMKillerEnabled,
oomKillerDisabled: options.OOMKillerDisabled,
oomMemoryLimit: options.OOMMemoryLimit,
// workingDirectory: options.WorkingDirectory,
// tempDirectory: options.TempDirectory,
// userID: options.UserID,
@@ -682,7 +696,42 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
if err != nil {
return nil, err
}
return nil, err
return &emptypb.Empty{}, nil
}
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
if !s.debug {
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
}
if request == nil {
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
}
switch request.Type {
case DebugCrashRequest_GO:
time.AfterFunc(200*time.Millisecond, func() {
*(*int)(unsafe.Pointer(uintptr(0))) = 0
})
case DebugCrashRequest_NATIVE:
err := s.handler.TriggerNativeCrash()
if err != nil {
return nil, err
}
default:
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
}
return &emptypb.Empty{}, nil
}
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
instance := s.Instance()
if instance == nil {
return nil, status.Error(codes.FailedPrecondition, "service not started")
}
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
if reporter == nil {
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
}
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
}
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
@@ -1019,9 +1068,12 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
return &DeprecatedWarnings{
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
return &DeprecatedWarning{
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Message: it.Message(),
Impending: it.Impending(),
MigrationLink: it.MigrationLink,
Description: it.Description,
DeprecatedVersion: it.DeprecatedVersion,
ScheduledVersion: it.ScheduledVersion,
}
}),
}, nil
@@ -1033,6 +1085,386 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
}
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
subscription, done, err := s.urlTestObserver.Subscribe()
if err != nil {
return err
}
defer s.urlTestObserver.UnSubscribe(subscription)
for {
s.serviceAccess.RLock()
if s.serviceStatus.Status != ServiceStatus_STARTED {
s.serviceAccess.RUnlock()
return os.ErrInvalid
}
boxService := s.instance
s.serviceAccess.RUnlock()
historyStorage := boxService.urlTestHistoryStorage
var list OutboundList
for _, ob := range boxService.instance.Outbound().Outbounds() {
item := &GroupItem{
Tag: ob.Tag(),
Type: ob.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
for _, ep := range boxService.instance.Endpoint().Endpoints() {
item := &GroupItem{
Tag: ep.Tag(),
Type: ep.Type(),
}
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
item.UrlTestTime = history.Time.Unix()
item.UrlTestDelay = int32(history.Delay)
}
list.Outbounds = append(list.Outbounds, item)
}
err = server.Send(&list)
if err != nil {
return err
}
select {
case <-subscription:
case <-s.ctx.Done():
return s.ctx.Err()
case <-server.Context().Done():
return server.Context().Err()
case <-done:
return nil
}
}
}
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
if tag == "" {
return instance.instance.Outbound().Default(), nil
}
outbound, loaded := instance.instance.Outbound().Outbound(tag)
if !loaded {
return nil, E.New("outbound not found: ", tag)
}
return outbound, nil
}
func (s *StartedService) StartNetworkQualityTest(
request *NetworkQualityTestRequest,
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
outbound, err := resolveOutbound(boxService, request.OutboundTag)
if err != nil {
return err
}
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
httpClient := networkquality.NewHTTPClient(resolvedDialer)
defer httpClient.CloseIdleConnections()
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
if err != nil {
return err
}
result, nqErr := networkquality.Run(networkquality.Options{
ConfigURL: request.ConfigURL,
HTTPClient: httpClient,
NewMeasurementClient: measurementClientFactory,
Serial: request.Serial,
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
Context: server.Context(),
OnProgress: func(p networkquality.Progress) {
_ = server.Send(&NetworkQualityTestProgress{
Phase: int32(p.Phase),
DownloadCapacity: p.DownloadCapacity,
UploadCapacity: p.UploadCapacity,
DownloadRPM: p.DownloadRPM,
UploadRPM: p.UploadRPM,
IdleLatencyMs: p.IdleLatencyMs,
ElapsedMs: p.ElapsedMs,
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
})
},
})
if nqErr != nil {
return server.Send(&NetworkQualityTestProgress{
IsFinal: true,
Error: nqErr.Error(),
})
}
return server.Send(&NetworkQualityTestProgress{
Phase: int32(networkquality.PhaseDone),
DownloadCapacity: result.DownloadCapacity,
UploadCapacity: result.UploadCapacity,
DownloadRPM: result.DownloadRPM,
UploadRPM: result.UploadRPM,
IdleLatencyMs: result.IdleLatencyMs,
IsFinal: true,
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
})
}
func (s *StartedService) StartSTUNTest(
request *STUNTestRequest,
server grpc.ServerStreamingServer[STUNTestProgress],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
outbound, err := resolveOutbound(boxService, request.OutboundTag)
if err != nil {
return err
}
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
result, stunErr := stun.Run(stun.Options{
Server: request.Server,
Dialer: resolvedDialer,
Context: server.Context(),
OnProgress: func(p stun.Progress) {
_ = server.Send(&STUNTestProgress{
Phase: int32(p.Phase),
ExternalAddr: p.ExternalAddr,
LatencyMs: p.LatencyMs,
NatMapping: int32(p.NATMapping),
NatFiltering: int32(p.NATFiltering),
})
},
})
if stunErr != nil {
return server.Send(&STUNTestProgress{
IsFinal: true,
Error: stunErr.Error(),
})
}
return server.Send(&STUNTestProgress{
Phase: int32(stun.PhaseDone),
ExternalAddr: result.ExternalAddr,
LatencyMs: result.LatencyMs,
NatMapping: int32(result.NATMapping),
NatFiltering: int32(result.NATFiltering),
IsFinal: true,
NatTypeSupported: result.NATTypeSupported,
})
}
func (s *StartedService) SubscribeTailscaleStatus(
_ *emptypb.Empty,
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
if endpointManager == nil {
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
type tailscaleEndpoint struct {
tag string
provider adapter.TailscaleEndpoint
}
var endpoints []tailscaleEndpoint
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
continue
}
endpoints = append(endpoints, tailscaleEndpoint{
tag: endpoint.Tag(),
provider: provider,
})
}
if len(endpoints) == 0 {
return status.Error(codes.NotFound, "no Tailscale endpoint found")
}
type taggedStatus struct {
tag string
status *adapter.TailscaleEndpointStatus
}
updates := make(chan taggedStatus, len(endpoints))
ctx, cancel := context.WithCancel(server.Context())
defer cancel()
var waitGroup sync.WaitGroup
for _, endpoint := range endpoints {
waitGroup.Add(1)
go func(tag string, provider adapter.TailscaleEndpoint) {
defer waitGroup.Done()
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
select {
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
case <-ctx.Done():
}
})
}(endpoint.tag, endpoint.provider)
}
go func() {
waitGroup.Wait()
close(updates)
}()
var tags []string
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
for update := range updates {
if _, exists := statuses[update.tag]; !exists {
tags = append(tags, update.tag)
}
statuses[update.tag] = update.status
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
for _, tag := range tags {
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
}
sendErr := server.Send(&TailscaleStatusUpdate{
Endpoints: protoEndpoints,
})
if sendErr != nil {
return sendErr
}
}
return nil
}
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
for i, group := range s.UserGroups {
peers := make([]*TailscalePeer, len(group.Peers))
for j, peer := range group.Peers {
peers[j] = tailscalePeerToProto(peer)
}
userGroups[i] = &TailscaleUserGroup{
UserID: group.UserID,
LoginName: group.LoginName,
DisplayName: group.DisplayName,
ProfilePicURL: group.ProfilePicURL,
Peers: peers,
}
}
result := &TailscaleEndpointStatus{
EndpointTag: tag,
BackendState: s.BackendState,
AuthURL: s.AuthURL,
NetworkName: s.NetworkName,
MagicDNSSuffix: s.MagicDNSSuffix,
UserGroups: userGroups,
}
if s.Self != nil {
result.Self = tailscalePeerToProto(s.Self)
}
return result
}
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
return &TailscalePeer{
HostName: peer.HostName,
DnsName: peer.DNSName,
Os: peer.OS,
TailscaleIPs: peer.TailscaleIPs,
Online: peer.Online,
ExitNode: peer.ExitNode,
ExitNodeOption: peer.ExitNodeOption,
Active: peer.Active,
RxBytes: peer.RxBytes,
TxBytes: peer.TxBytes,
KeyExpiry: peer.KeyExpiry,
}
}
func (s *StartedService) StartTailscalePing(
request *TailscalePingRequest,
server grpc.ServerStreamingServer[TailscalePingResponse],
) error {
err := s.waitForStarted(server.Context())
if err != nil {
return err
}
s.serviceAccess.RLock()
boxService := s.instance
s.serviceAccess.RUnlock()
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
if endpointManager == nil {
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
}
var provider adapter.TailscaleEndpoint
if request.EndpointTag != "" {
endpoint, loaded := endpointManager.Get(request.EndpointTag)
if !loaded {
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
}
if endpoint.Type() != C.TypeTailscale {
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if !loaded {
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
}
provider = pingProvider
} else {
for _, endpoint := range endpointManager.Endpoints() {
if endpoint.Type() != C.TypeTailscale {
continue
}
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
if loaded {
provider = pingProvider
break
}
}
if provider == nil {
return status.Error(codes.NotFound, "no Tailscale endpoint found")
}
}
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
_ = server.Send(&TailscalePingResponse{
LatencyMs: result.LatencyMs,
IsDirect: result.IsDirect,
Endpoint: result.Endpoint,
DerpRegionID: result.DERPRegionID,
DerpRegionCode: result.DERPRegionCode,
Error: result.Error,
})
})
}
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,12 +26,20 @@ service StartedService {
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
}
message ServiceStatus {
@@ -141,6 +149,15 @@ message SetSystemProxyEnabledRequest {
bool enabled = 1;
}
message DebugCrashRequest {
enum Type {
GO = 0;
NATIVE = 1;
}
Type type = 1;
}
message SubscribeConnectionsRequest {
int64 interval = 1;
}
@@ -210,8 +227,105 @@ message DeprecatedWarning {
string message = 1;
bool impending = 2;
string migrationLink = 3;
string description = 4;
string deprecatedVersion = 5;
string scheduledVersion = 6;
}
message StartedAt {
int64 startedAt = 1;
}
}
message OutboundList {
repeated GroupItem outbounds = 1;
}
message NetworkQualityTestRequest {
string configURL = 1;
string outboundTag = 2;
bool serial = 3;
int32 maxRuntimeSeconds = 4;
bool http3 = 5;
}
message NetworkQualityTestProgress {
int32 phase = 1;
int64 downloadCapacity = 2;
int64 uploadCapacity = 3;
int32 downloadRPM = 4;
int32 uploadRPM = 5;
int32 idleLatencyMs = 6;
int64 elapsedMs = 7;
bool isFinal = 8;
string error = 9;
int32 downloadCapacityAccuracy = 10;
int32 uploadCapacityAccuracy = 11;
int32 downloadRPMAccuracy = 12;
int32 uploadRPMAccuracy = 13;
}
message STUNTestRequest {
string server = 1;
string outboundTag = 2;
}
message STUNTestProgress {
int32 phase = 1;
string externalAddr = 2;
int32 latencyMs = 3;
int32 natMapping = 4;
int32 natFiltering = 5;
bool isFinal = 6;
string error = 7;
bool natTypeSupported = 8;
}
message TailscaleStatusUpdate {
repeated TailscaleEndpointStatus endpoints = 1;
}
message TailscaleEndpointStatus {
string endpointTag = 1;
string backendState = 2;
string authURL = 3;
string networkName = 4;
string magicDNSSuffix = 5;
TailscalePeer self = 6;
repeated TailscaleUserGroup userGroups = 7;
}
message TailscaleUserGroup {
int64 userID = 1;
string loginName = 2;
string displayName = 3;
string profilePicURL = 4;
repeated TailscalePeer peers = 5;
}
message TailscalePeer {
string hostName = 1;
string dnsName = 2;
string os = 3;
repeated string tailscaleIPs = 4;
bool online = 5;
bool exitNode = 6;
bool exitNodeOption = 7;
bool active = 8;
int64 rxBytes = 9;
int64 txBytes = 10;
int64 keyExpiry = 11;
}
message TailscalePingRequest {
string endpointTag = 1;
string peerIP = 2;
}
message TailscalePingResponse {
double latencyMs = 1;
bool isDirect = 2;
string endpoint = 3;
int32 derpRegionID = 4;
string derpRegionCode = 5;
string error = 6;
}

View File

@@ -15,27 +15,34 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
)
// StartedServiceClient is the client API for StartedService service.
@@ -58,11 +65,18 @@ type StartedServiceClient interface {
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error)
SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error)
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
}
type startedServiceClient struct {
@@ -278,6 +292,26 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
return out, nil
}
func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
@@ -337,6 +371,101 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
return out, nil
}
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList]
func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress]
func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate]
func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse]
// StartedServiceServer is the server API for StartedService service.
// All implementations must embed UnimplementedStartedServiceServer
// for forward compatibility.
@@ -357,11 +486,18 @@ type StartedServiceServer interface {
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error
SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
mustEmbedUnimplementedStartedServiceServer()
}
@@ -436,6 +572,14 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
}
func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
}
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
}
@@ -455,6 +599,26 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
}
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
}
func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error {
return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented")
}
func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error {
return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented")
}
func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error {
return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented")
}
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
@@ -729,6 +893,42 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DebugCrashRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerDebugCrash_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StartedServiceServer).TriggerOOMReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(SubscribeConnectionsRequest)
if err := stream.RecvMsg(m); err != nil {
@@ -812,6 +1012,61 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
return interceptor(ctx, in, info, handler)
}
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList]
func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(NetworkQualityTestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(STUNTestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress]
func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(emptypb.Empty)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate]
func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(TailscalePingRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse]
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -863,6 +1118,14 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetSystemProxyEnabled",
Handler: _StartedService_SetSystemProxyEnabled_Handler,
},
{
MethodName: "TriggerDebugCrash",
Handler: _StartedService_TriggerDebugCrash_Handler,
},
{
MethodName: "TriggerOOMReport",
Handler: _StartedService_TriggerOOMReport_Handler,
},
{
MethodName: "CloseConnection",
Handler: _StartedService_CloseConnection_Handler,
@@ -911,6 +1174,31 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
Handler: _StartedService_SubscribeConnections_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeOutbounds",
Handler: _StartedService_SubscribeOutbounds_Handler,
ServerStreams: true,
},
{
StreamName: "StartNetworkQualityTest",
Handler: _StartedService_StartNetworkQualityTest_Handler,
ServerStreams: true,
},
{
StreamName: "StartSTUNTest",
Handler: _StartedService_StartSTUNTest_Handler,
ServerStreams: true,
},
{
StreamName: "SubscribeTailscaleStatus",
Handler: _StartedService_SubscribeTailscaleStatus_Handler,
ServerStreams: true,
},
{
StreamName: "StartTailscalePing",
Handler: _StartedService_StartTailscalePing_Handler,
ServerStreams: true,
},
},
Metadata: "daemon/started_service.proto",
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"net"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -14,7 +13,6 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/contrab/freelru"
"github.com/sagernet/sing/contrab/maphash"
@@ -32,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
}
@@ -93,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) {
@@ -109,7 +124,38 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
func 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 {
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
@@ -123,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 &&
@@ -141,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
}
}
}
@@ -190,62 +222,17 @@ 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
// TODO: add accept_any rule and support to check response instead of addresses
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
rejected = true
} else if len(response.Answer) == 0 {
rejected = !responseChecker(nil)
} else {
rejected = !responseChecker(MessageToAddresses(response))
rejected = !responseChecker(response)
}
if rejected {
if !disableCache && c.rdrc != nil {
@@ -255,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)
}
@@ -321,7 +261,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return response, nil
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain)
var strategy C.DomainStrategy
@@ -368,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)
}
}
}
@@ -385,46 +329,44 @@ 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))
}
}
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
question := dns.Question{
Name: name,
Qtype: qType,
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
@@ -435,120 +377,181 @@ 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 {
if response == nil || response.Rcode != dns.RcodeSuccess {
return nil
}
addresses := make([]netip.Addr, 0, len(response.Answer))
for _, rawAnswer := range response.Answer {
switch answer := rawAnswer.(type) {
case *dns.A:
addresses = append(addresses, M.AddrFromIP(answer.A))
case *dns.AAAA:
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
case *dns.HTTPS:
for _, value := range answer.SVCB.Value {
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
}
}
}
}
return addresses
return adapter.DNSResponseAddresses(response)
}
func wrapError(err error) error {

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

@@ -5,10 +5,11 @@ import (
)
const (
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
RcodeSuccess RcodeError = mDNS.RcodeSuccess
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
RcodeFormatError RcodeError = mDNS.RcodeFormatError
RcodeNameError RcodeError = mDNS.RcodeNameError
RcodeRefused RcodeError = mDNS.RcodeRefused
)
type RcodeError int

111
dns/repro_test.go Normal file
View File

@@ -0,0 +1,111 @@
package dns
import (
"context"
"net/netip"
"testing"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
mDNS "github.com/miekg/dns"
"github.com/stretchr/testify/require"
)
func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
t.Parallel()
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
var qTypes []uint16
router := newTestRouter(t, nil, &fakeDNSTransportManager{
defaultTransport: defaultTransport,
transports: map[string]adapter.DNSTransport{
"default": defaultTransport,
},
}, &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
qTypes = append(qTypes, message.Question[0].Qtype)
if message.Question[0].Qtype == mDNS.TypeA {
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
}
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
},
})
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
Strategy: C.DomainStrategyIPv4Only,
})
require.NoError(t, err)
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
}
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
t.Parallel()
transportManager := &fakeDNSTransportManager{
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
transports: map[string]adapter.DNSTransport{
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
},
}
client := &fakeDNSClient{
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
switch transport.Tag() {
case "upstream":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
case "selected":
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
default:
return nil, E.New("unexpected transport")
}
},
}
rules := []option.DNSRule{
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
Domain: badoption.Listable[string]{"example.com"},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeEvaluate,
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
},
},
},
{
Type: C.RuleTypeLogical,
LogicalOptions: option.LogicalDNSRule{
RawLogicalDNSRule: option.RawLogicalDNSRule{
Mode: C.LogicalTypeOr,
Rules: []option.DNSRule{{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultDNSRule{
RawDefaultDNSRule: option.RawDefaultDNSRule{
MatchResponse: true,
IPCIDR: badoption.Listable[string]{"1.1.1.0/24"},
},
},
}},
},
DNSRuleAction: option.DNSRuleAction{
Action: C.RuleActionTypeRoute,
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
},
},
},
}
router := newTestRouter(t, rules, transportManager, client)
response, err := router.Exchange(context.Background(), &mDNS.Msg{
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
}, adapter.DNSQueryOptions{})
require.NoError(t, err)
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
}

File diff suppressed because it is too large Load Diff

2547
dns/router_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,6 @@ package local
import (
"context"
"errors"
"net"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
@@ -14,7 +12,6 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -35,10 +32,8 @@ type Transport struct {
logger logger.ContextLogger
hosts *hosts.File
dialer N.Dialer
preferGo bool
fallback bool
dhcpTransport dhcpTransport
resolver net.Resolver
}
type dhcpTransport interface {
@@ -52,14 +47,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
if err != nil {
return nil, err
}
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
return &Transport{
TransportAdapter: transportAdapter,
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
ctx: ctx,
logger: logger,
hosts: hosts.NewFile(hosts.DefaultPath),
dialer: transportDialer,
preferGo: options.PreferGo,
}, nil
}
@@ -97,44 +90,3 @@ func (t *Transport) Reset() {
t.dhcpTransport.Reset()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
if !t.fallback {
return t.exchange(ctx, message, question.Name)
}
if t.dhcpTransport != nil {
dhcpTransports := t.dhcpTransport.Fetch()
if len(dhcpTransports) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
}
}
if t.preferGo {
// Assuming the user knows what they are doing, we still execute the query which will fail.
return t.exchange(ctx, message, question.Name)
}
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
if question.Qtype == mDNS.TypeA {
network = "ip4"
} else {
network = "ip6"
}
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, dns.RcodeRefused
}
return nil, err
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
}

View File

@@ -0,0 +1,249 @@
//go:build darwin
package local
/*
#include <stdlib.h>
#include <dns.h>
#include <resolv.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));
if (state == NULL) return NULL;
if (res_ninit(state) != 0) {
free(state);
return NULL;
}
return state;
}
static void cgo_res_destroy(void *opaque) {
res_state state = (res_state)opaque;
res_ndestroy(state);
free(state);
}
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
unsigned char *answer, int anslen,
int timeout_seconds,
int *out_h_errno) {
res_state state = (res_state)opaque;
state->retrans = timeout_seconds;
state->retry = 1;
int n = res_nsearch(state, dname, class, type, answer, anslen);
if (n < 0) {
*out_h_errno = state->res_h_errno;
}
return n;
}
*/
import "C"
import (
"context"
"errors"
"time"
"unsafe"
boxC "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
)
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")
}
defer C.cgo_res_destroy(state)
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
bufSize := 1232
for {
answer := make([]byte, bufSize)
var hErrno C.int
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
C.int(timeoutSeconds),
&hErrno)
if n >= 0 {
if int(n) > bufSize {
bufSize = int(n)
continue
}
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
}
continue
}
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)
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
}
}
if t.fallback && t.dhcpTransport != nil {
dhcpServers := t.dhcpTransport.Fetch()
if len(dhcpServers) > 0 {
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
}
}
name := question.Name
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, context.DeadlineExceeded
}
seconds := int(remaining.Seconds())
if seconds < 1 {
seconds = 1
}
timeoutSeconds = seconds
}
type resolvResult struct {
response *mDNS.Msg
err error
}
resultCh := make(chan resolvResult, 1)
go func() {
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
resultCh <- resolvResult{response, err}
}()
var result resolvResult
select {
case <-ctx.Done():
return nil, ctx.Err()
case result = <-resultCh:
}
if result.err != nil {
var rcodeError dns.RcodeError
if errors.As(result.err, &rcodeError) {
return dns.FixedResponseStatus(message, int(rcodeError)), nil
}
return nil, result.err
}
result.response.Id = message.Id
return result.response, nil
}

View File

@@ -1,3 +1,5 @@
//go:build !darwin
package local
import (

View File

@@ -1,21 +1,13 @@
package dns
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
)
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
type TransportAdapter struct {
transportType string
transportTag string
dependencies []string
strategy C.DomainStrategy
clientSubnet netip.Prefix
}
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
@@ -35,8 +27,6 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
clientSubnet: localOptions.LegacyClientSubnet,
}
}
@@ -45,15 +35,10 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
}
if remoteOptions.LegacyAddressResolver != "" {
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
clientSubnet: remoteOptions.LegacyClientSubnet,
}
}
@@ -68,11 +53,3 @@ func (a *TransportAdapter) Tag() string {
func (a *TransportAdapter) Dependencies() []string {
return a.dependencies
}
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
return a.strategy
}
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
return a.clientSubnet
}

View File

@@ -2,104 +2,25 @@ package dns
import (
"context"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
if options.LegacyDefaultDialer {
return dialer.NewDefaultOutbound(ctx), nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
DirectResolver: true,
})
}
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
if options.LegacyDefaultDialer {
transportDialer := dialer.NewDefaultOutbound(ctx)
if options.LegacyAddressResolver != "" {
transport := service.FromContext[adapter.DNSTransportManager](ctx)
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
if !loaded {
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
}
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
} else if options.ServerIsDomain() {
return nil, E.New("missing address resolver for server: ", options.Server)
}
return transportDialer, nil
} else {
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
LegacyDNSDialer: options.Legacy,
})
}
}
type legacyTransportDialer struct {
dialer N.Dialer
dnsRouter adapter.DNSRouter
transport adapter.DNSTransport
strategy C.DomainStrategy
fallbackDelay time.Duration
}
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
return &legacyTransportDialer{
dialer,
dnsRouter,
transport,
strategy,
fallbackDelay,
}
}
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if destination.IsIP() {
return d.dialer.DialContext(ctx, network, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
return dialer.NewWithOptions(dialer.Options{
Context: ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
DirectResolver: true,
})
if err != nil {
return nil, err
}
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
}
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
if destination.IsIP() {
return d.dialer.ListenPacket(ctx, destination)
}
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
Transport: d.transport,
Strategy: d.strategy,
})
if err != nil {
return nil, err
}
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
return conn, err
}
func (d *legacyTransportDialer) Upstream() any {
return d.dialer
}

View File

@@ -2,18 +2,109 @@
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.14.0-alpha.8
* Add BBR profile and hop interval randomization for Hysteria2 **1**
* Fixes and improvements
**1**:
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
#### 1.14.0-alpha.8
* Fixes and improvements
#### 1.13.5
* Fixes and improvements
#### 1.14.0-alpha.7
* Fixes and improvements
#### 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**
@@ -38,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
@@ -659,7 +803,7 @@ DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
Compatibility for old formats will be removed in sing-box 1.14.0.
@@ -1129,7 +1273,7 @@ DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
Compatibility for old formats will be removed in sing-box 1.14.0.
@@ -1965,7 +2109,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
@@ -1979,7 +2123,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
**5**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
**6**:
@@ -2160,7 +2304,7 @@ See [TUN](/configuration/inbound/tun) inbound.
**1**:
The new feature allows you to cache the check results of
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
#### 1.9.0-alpha.7
@@ -2207,7 +2351,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.

View File

@@ -42,6 +42,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
| `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-check: | / |
| `package_name_regex` | :material-check: | / |
| `user` | :material-close: | Use `package_name` instead |
| `user_id` | :material-close: | Use `package_name` instead |
| `wifi_ssid` | :material-check: | Fine location permission required |

View File

@@ -44,6 +44,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
| `process_path` | :material-close: | No permission |
| `process_path_regex` | :material-close: | No permission |
| `package_name` | :material-close: | / |
| `package_name_regex` | :material-close: | / |
| `user` | :material-close: | No permission |
| `user_id` | :material-close: | No permission |
| `wifi_ssid` | :material-alert: | Only supported on iOS |

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "Deprecated in sing-box 1.12.0"
!!! failure "Removed in sing-box 1.14.0"
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
### Structure
@@ -26,6 +26,6 @@ Enable FakeIP service.
IPv4 address range for FakeIP.
#### inet6_address
#### inet6_range
IPv6 address range for FakeIP.

View File

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

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": {}
@@ -39,7 +45,7 @@ icon: material/alert-decagram
|----------|---------------------------------|
| `server` | List of [DNS Server](./server/) |
| `rules` | List of [DNS Rule](./rule/) |
| `fakeip` | [FakeIP](./fakeip/) |
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
#### final
@@ -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.
@@ -88,4 +130,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.

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 地址的反向映射以为路由目的提供域名。
@@ -88,6 +130,6 @@ LRU 缓存容量。
可以被 `servers.[].client_subnet``rules.[].client_subnet` 覆盖。
#### fakeip
#### fakeip :material-note-remove:
[FakeIP](./fakeip/) 设置。

View File

@@ -2,6 +2,20 @@
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [match_response](#match_response)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [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"
:material-plus: [interface_address](#interface_address)
@@ -89,12 +103,6 @@ icon: material/alert-decagram
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"source_port": [
12345
],
@@ -124,6 +132,9 @@ icon: material/alert-decagram
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -149,6 +160,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -160,7 +177,17 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"match_response": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -169,7 +196,8 @@ icon: material/alert-decagram
"server": "local",
// Deprecated
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -217,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
@@ -325,6 +387,12 @@ Match process path using regular expression.
Match android package name.
#### package_name_regex
!!! question "Since sing-box 1.14.0"
Match android package name using regular expression.
#### user
!!! quote ""
@@ -408,6 +476,26 @@ Matches network interface (same values as `network_type`) address.
Match default interface address.
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### wifi_ssid
!!! quote ""
@@ -446,6 +534,25 @@ Make `ip_cidr` rule items in rule-sets match the source IP.
Make `ip_cidr` rule items in rule-sets match the source IP.
#### match_response
!!! question "Since sing-box 1.14.0"
Enable response-based matching. When enabled, this rule matches against the evaluated response
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
instead of only matching the original query.
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields.
#### ip_accept_any
!!! question "Since sing-box 1.12.0"
Match when the DNS query response contains at least one address.
#### invert
Invert match result.
@@ -490,7 +597,12 @@ See [DNS Rule Actions](../rule_action/) for details.
Moved to [DNS Rule Action](../rule_action#route).
### Address Filter Fields
### Legacy Address Filter Fields
!!! failure "Deprecated in sing-box 1.14.0"
Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
@@ -516,23 +628,61 @@ Match GeoIP with query response.
Match IP CIDR with query response.
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
#### rule_set_ip_cidr_accept_empty
!!! question "Since sing-box 1.10.0"
!!! failure "Deprecated in sing-box 1.14.0"
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0,
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
Make `ip_cidr` rules in rule-sets accept empty query response.
#### ip_accept_any
### Response Match Fields
!!! question "Since sing-box 1.12.0"
!!! question "Since sing-box 1.14.0"
Match any IP with query response.
Match fields for the evaluated response. Require `match_response` to be set to `true`
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
#### response_rcode
Match DNS response code.
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
#### response_answer
Match DNS answer records.
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
#### response_ns
Match DNS name server records.
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
#### response_extra
Match DNS extra records.
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
### Logical Fields

View File

@@ -2,6 +2,20 @@
icon: material/alert-decagram
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [match_response](#match_response)
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
:material-plus: [response_rcode](#response_rcode)
:material-plus: [response_answer](#response_answer)
:material-plus: [response_ns](#response_ns)
:material-plus: [response_extra](#response_extra)
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [ip_version](#ip_version)
:material-alert: [query_type](#query_type)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -89,12 +103,6 @@ icon: material/alert-decagram
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"source_port": [
12345
],
@@ -124,6 +132,9 @@ icon: material/alert-decagram
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -149,6 +160,12 @@ icon: material/alert-decagram
"default_interface_address": [
"2000::/3"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"wifi_ssid": [
"My WIFI"
],
@@ -160,7 +177,17 @@ icon: material/alert-decagram
"geosite-cn"
],
"rule_set_ip_cidr_match_source": false,
"rule_set_ip_cidr_accept_empty": false,
"match_response": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"ip_accept_any": false,
"response_rcode": "",
"response_answer": [],
"response_ns": [],
"response_extra": [],
"invert": false,
"outbound": [
"direct"
@@ -169,6 +196,8 @@ icon: material/alert-decagram
"server": "local",
// 已弃用
"rule_set_ip_cidr_accept_empty": false,
"rule_set_ipcidr_match_source": false,
"geosite": [
"cn"
@@ -216,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
@@ -324,6 +379,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配 Android 应用包名。
#### package_name_regex
!!! question "自 sing-box 1.14.0 起"
使用正则表达式匹配 Android 应用包名。
#### user
!!! quote ""
@@ -407,6 +468,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
匹配默认接口地址。
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### wifi_ssid
!!! quote ""
@@ -445,6 +526,23 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
使规则集中的 `ip_cidr` 规则匹配源 IP。
#### match_response
!!! question "自 sing-box 1.14.0 起"
启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
响应匹配字段(`response_rcode``response_answer``response_ns``response_extra`)需要此选项。
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr``ip_is_private``ip_accept_any` 也需要此选项。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
当 DNS 查询响应包含至少一个地址时匹配。
#### invert
反选匹配结果。
@@ -489,7 +587,12 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
已移动到 [DNS 规则动作](../rule_action#route).
### 地址筛选字段
### 旧版地址筛选字段
!!! failure "已在 sing-box 1.14.0 废弃"
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
@@ -516,24 +619,62 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
与查询响应匹配 IP CIDR。
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
#### ip_is_private
!!! question "自 sing-box 1.9.0 起"
与查询响应匹配非公开 IP。
#### ip_accept_any
!!! question "自 sing-box 1.12.0 起"
匹配任意 IP。
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
#### rule_set_ip_cidr_accept_empty
!!! question "自 sing-box 1.10.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除,
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
使规则集中的 `ip_cidr` 规则接受空查询响应。
### 响应匹配字段
!!! question "自 sing-box 1.14.0 起"
已评估的响应的匹配字段。需要将 `match_response` 设为 `true`
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
#### response_rcode
匹配 DNS 响应码。
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
#### response_answer
匹配 DNS 应答记录。
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
#### response_ns
匹配 DNS 名称服务器记录。
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
#### response_extra
匹配 DNS 额外记录。
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
### 逻辑字段
#### type

View File

@@ -2,6 +2,13 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [strategy](#strategy)
@@ -17,6 +24,7 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -34,6 +42,10 @@ Tag of target server.
!!! question "Since sing-box 1.12.0"
!!! failure "Deprecated in sing-box 1.14.0"
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
Set domain strategy for this query.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
@@ -42,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.
@@ -52,7 +70,75 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.
### evaluate
!!! question "Since sing-box 1.14.0"
```json
{
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
Unlike `route`, it does **not** terminate rule evaluation.
Only allowed on top-level DNS rules (not inside logical sub-rules).
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
does not satisfy this requirement, because matching happens before the action runs.
#### server
==Required==
Tag of target server.
#### disable_cache
Disable cache and save cache in this query.
#### disable_optimistic_cache
!!! question "Since sing-box 1.14.0"
Disable optimistic DNS caching in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
#### client_subnet
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will override `dns.client_subnet`.
### respond
!!! question "Since sing-box 1.14.0"
```json
{
"action": "respond"
}
```
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
This action does not send a new DNS query and has no extra options.
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
### route-options
@@ -60,6 +146,7 @@ Will overrides `dns.client_subnet`.
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}

View File

@@ -2,6 +2,13 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-delete-clock: [strategy](#strategy)
:material-plus: [evaluate](#evaluate)
:material-plus: [respond](#respond)
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [strategy](#strategy)
@@ -17,6 +24,7 @@ icon: material/new-box
"server": "",
"strategy": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -34,6 +42,10 @@ icon: material/new-box
!!! question "自 sing-box 1.12.0 起"
!!! failure "已在 sing-box 1.14.0 废弃"
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
为此查询设置域名策略。
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`
@@ -42,6 +54,12 @@ icon: material/new-box
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
@@ -54,12 +72,79 @@ icon: material/new-box
将覆盖 `dns.client_subnet`.
### evaluate
!!! question "自 sing-box 1.14.0 起"
```json
{
"action": "evaluate",
"server": "",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
```
`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
因为匹配发生在动作执行之前。
#### server
==必填==
目标 DNS 服务器的标签。
#### disable_cache
在此查询中禁用缓存。
#### disable_optimistic_cache
!!! question "自 sing-box 1.14.0 起"
在此查询中禁用乐观 DNS 缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
#### client_subnet
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
如果值是 IP 地址而不是前缀,则会自动附加 `/32``/128`
将覆盖 `dns.client_subnet`.
### respond
!!! question "自 sing-box 1.14.0 起"
```json
{
"action": "respond"
}
```
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
此动作不会发起新的 DNS 查询,也没有额外选项。
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
### route-options
```json
{
"action": "route-options",
"disable_cache": false,
"disable_optimistic_cache": false,
"rewrite_ttl": null,
"client_subnet": null
}
@@ -84,7 +169,7 @@ icon: material/new-box
- `default`: 返回 REFUSED。
- `drop`: 丢弃请求。
默认使用 `defualt`
默认使用 `default`
#### no_drop

View File

@@ -73,24 +73,55 @@ Example:
=== "Use hosts if available"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"action": "evaluate",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
]
}
]
}
}
```
}
```

View File

@@ -73,24 +73,55 @@ hosts 文件路径列表。
=== "如果可用则使用 hosts"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"action": "evaluate",
"server": "hosts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
...
},
{
"type": "hosts",
"tag": "hosts"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "hosts"
}
]
}
]
}
}
```
}
```

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
---
icon: material/delete-clock
icon: material/note-remove
---
!!! failure "Deprecated in sing-box 1.12.0"
!!! failure "Removed in sing-box 1.14.0"
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
!!! quote "Changes in sing-box 1.9.0"
@@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Can be overrides by `rules.[].client_subnet`.
Can be overridden by `rules.[].client_subnet`.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.

View File

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

View File

@@ -43,29 +43,62 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
=== "Split DNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"action": "evaluate",
"server": "resolved"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
]
}
}
```
}
```
=== "Use as global DNS"

View File

@@ -42,29 +42,62 @@ icon: material/new-box
=== "仅分割 DNS"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"action": "evaluate",
"server": "resolved"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
]
}
}
```
}
```
=== "用作全局 DNS"

View File

@@ -42,29 +42,62 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
=== "MagicDNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"action": "evaluate",
"server": "ts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
]
}
]
}
}
```
}
```
=== "Use as global DNS"

View File

@@ -42,29 +42,62 @@ icon: material/new-box
=== "仅 MagicDNS"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
=== ":material-card-multiple: sing-box 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"action": "evaluate",
"server": "ts"
},
{
"match_response": true,
"ip_accept_any": true,
"action": "respond"
}
]
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
```
=== ":material-card-remove: sing-box < 1.14.0"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "tailscale",
"tag": "ts",
"endpoint": "ts-ep"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "ts"
}
]
}
]
}
}
```
}
```
=== "用作全局 DNS"

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,9 +48,13 @@ 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 [Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields)
The check results of [Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields)
will be cached until expiration.
#### rdrc_timeout
@@ -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,12 +46,22 @@
#### 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 响应缓存存储在缓存文件中。
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
[旧版地址筛选字段](/zh/configuration/dns/rule/#旧版地址筛选字段) 的检查结果将被缓存至过期。
#### rdrc_timeout
拒绝的 DNS 响应缓存超时。
默认使用 `7d`
#### store_dns
!!! question "自 sing-box 1.14.0 起"
将 DNS 缓存存储在缓存文件中。

View File

@@ -0,0 +1,89 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
`cloudflared` inbound runs an embedded Cloudflare Tunnel client and routes all
incoming tunnel traffic (TCP, UDP, ICMP) through sing-box's routing engine.
### Structure
```json
{
"type": "cloudflared",
"tag": "",
"token": "",
"ha_connections": 0,
"protocol": "",
"post_quantum": false,
"edge_ip_version": 0,
"datagram_version": "",
"grace_period": "",
"region": "",
"control_dialer": {
... // Dial Fields
},
"tunnel_dialer": {
... // Dial Fields
}
}
```
### Fields
#### token
==Required==
Base64-encoded tunnel token from the Cloudflare Zero Trust dashboard
(`Networks → Tunnels → Install connector`).
#### ha_connections
Number of high-availability connections to the Cloudflare edge.
Capped by the number of discovered edge addresses.
#### protocol
Transport protocol for edge connections.
One of `quic` `http2`.
#### post_quantum
Enable post-quantum key exchange on the control connection.
#### edge_ip_version
IP version used when connecting to the Cloudflare edge.
One of `0` (automatic) `4` `6`.
#### datagram_version
Datagram protocol version used for UDP proxying over QUIC.
One of `v2` `v3`. Only meaningful when `protocol` is `quic`.
#### grace_period
Graceful shutdown window for in-flight edge connections.
#### region
Cloudflare edge region selector.
Conflict with endpoints embedded in `token`.
#### control_dialer
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
Cloudflare control plane.
#### tunnel_dialer
[Dial Fields](/configuration/shared/dial/) used when the tunnel client dials the
Cloudflare edge data plane.

View File

@@ -0,0 +1,89 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
`cloudflared` 入站运行一个内嵌的 Cloudflare Tunnel 客户端,并将所有传入的隧道流量
TCP、UDP、ICMP通过 sing-box 的路由引擎转发。
### 结构
```json
{
"type": "cloudflared",
"tag": "",
"token": "",
"ha_connections": 0,
"protocol": "",
"post_quantum": false,
"edge_ip_version": 0,
"datagram_version": "",
"grace_period": "",
"region": "",
"control_dialer": {
... // 拨号字段
},
"tunnel_dialer": {
... // 拨号字段
}
}
```
### 字段
#### token
==必填==
来自 Cloudflare Zero Trust 仪表板的 Base64 编码隧道令牌
`Networks → Tunnels → Install connector`)。
#### ha_connections
到 Cloudflare edge 的高可用连接数。
上限为已发现的 edge 地址数量。
#### protocol
edge 连接使用的传输协议。
`quic` `http2` 之一。
#### post_quantum
在控制连接上启用后量子密钥交换。
#### edge_ip_version
连接 Cloudflare edge 时使用的 IP 版本。
`0`(自动)`4` `6` 之一。
#### datagram_version
通过 QUIC 进行 UDP 代理时使用的数据报协议版本。
`v2` `v3` 之一。仅在 `protocol``quic` 时有效。
#### grace_period
正在处理的 edge 连接的优雅关闭窗口。
#### region
Cloudflare edge 区域选择器。
`token` 中嵌入的 endpoint 冲突。
#### control_dialer
隧道客户端拨向 Cloudflare 控制面时使用的
[拨号字段](/zh/configuration/shared/dial/)。
#### tunnel_dialer
隧道客户端拨向 Cloudflare edge 数据面时使用的
[拨号字段](/zh/configuration/shared/dial/)。

View File

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

View File

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

View File

@@ -34,6 +34,7 @@
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: |
#### tag

View File

@@ -34,6 +34,7 @@
| `tun` | [Tun](./tun/) | :material-close: |
| `redirect` | [Redirect](./redirect/) | :material-close: |
| `tproxy` | [TProxy](./tproxy/) | :material-close: |
| `cloudflared` | [Cloudflared](./cloudflared/) | :material-close: |
#### tag

View File

@@ -4,7 +4,7 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "Changes in sing-box 1.13.3"
@@ -134,6 +134,12 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -560,6 +566,30 @@ Limit android packages in route.
Exclude android packages in route.
#### include_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Limit MAC addresses in route. Not limited by default.
Conflict with `exclude_mac_address`.
#### exclude_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
Exclude MAC addresses in route.
Conflict with `include_mac_address`.
#### platform
Platform-specific settings, provided by client applications.

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [include_mac_address](#include_mac_address)
:material-plus: [exclude_mac_address](#exclude_mac_address)
!!! quote "sing-box 1.13.3 中的更改"
:material-alert: [strict_route](#strict_route)
@@ -130,6 +135,12 @@ icon: material/new-box
"exclude_package": [
"com.android.captiveportallogin"
],
"include_mac_address": [
"00:11:22:33:44:55"
],
"exclude_mac_address": [
"66:77:88:99:aa:bb"
],
"platform": {
"http_proxy": {
"enabled": false,
@@ -543,6 +554,30 @@ TCP/IP 栈。
排除路由的 Android 应用包名。
#### include_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
限制被路由的 MAC 地址。默认不限制。
`exclude_mac_address` 冲突。
#### exclude_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux且需要 `auto_route``auto_redirect` 已启用。
排除路由的 MAC 地址。
`include_mac_address` 冲突。
#### platform
平台特定的设置,由客户端应用提供。

View File

@@ -1,7 +1,6 @@
# Introduction
sing-box uses JSON for configuration files.
### Structure
```json
@@ -10,6 +9,7 @@ sing-box uses JSON for configuration files.
"dns": {},
"ntp": {},
"certificate": {},
"certificate_providers": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -27,6 +27,7 @@ sing-box uses JSON for configuration files.
| `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) |
| `certificate` | [Certificate](./certificate/) |
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
| `endpoints` | [Endpoint](./endpoint/) |
| `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) |
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
```bash
sing-box merge output.json -c config.json -D config_directory
```
```

View File

@@ -1,7 +1,6 @@
# 引言
sing-box 使用 JSON 作为配置文件格式。
### 结构
```json
@@ -10,6 +9,7 @@ sing-box 使用 JSON 作为配置文件格式。
"dns": {},
"ntp": {},
"certificate": {},
"certificate_providers": [],
"endpoints": [],
"inbounds": [],
"outbounds": [],
@@ -27,6 +27,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `dns` | [DNS](./dns/) |
| `ntp` | [NTP](./ntp/) |
| `certificate` | [证书](./certificate/) |
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
| `endpoints` | [端点](./endpoint/) |
| `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) |
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
```bash
sing-box merge output.json -c config.json -D config_directory
```
```

View File

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

View File

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

View File

@@ -4,6 +4,11 @@ icon: material/alert-decagram
# Route
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "Changes in sing-box 1.12.0"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -35,6 +40,9 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_domain_resolver": "", // or {}
"default_network_strategy": "",
"default_network_type": [],
@@ -107,13 +115,45 @@ Set routing mark by default.
Takes no effect if `outbound.routing_mark` is set.
#### find_process
!!! quote ""
Only supported on Linux, Windows, and macOS.
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
#### find_neighbor
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
#### dhcp_lease_files
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux and macOS.
Custom DHCP lease file paths for hostname and MAC address resolution.
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
#### default_domain_resolver
!!! question "Since sing-box 1.12.0"
See [Dial Fields](/configuration/shared/dial/#domain_resolver) for details.
Can be overrides by `outbound.domain_resolver`.
Can be overridden by `outbound.domain_resolver`.
#### default_network_strategy
@@ -123,7 +163,7 @@ See [Dial Fields](/configuration/shared/dial/#network_strategy) for details.
Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set.
Can be overrides by `outbound.network_strategy`.
Can be overridden by `outbound.network_strategy`.
Conflicts with `default_interface`.

View File

@@ -4,6 +4,11 @@ icon: material/alert-decagram
# 路由
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [find_neighbor](#find_neighbor)
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
!!! quote "sing-box 1.12.0 中的更改"
:material-plus: [default_domain_resolver](#default_domain_resolver)
@@ -37,6 +42,9 @@ icon: material/alert-decagram
"override_android_vpn": false,
"default_interface": "",
"default_mark": 0,
"find_process": false,
"find_neighbor": false,
"dhcp_lease_files": [],
"default_network_strategy": "",
"default_fallback_delay": ""
}
@@ -106,6 +114,38 @@ icon: material/alert-decagram
如果设置了 `outbound.routing_mark` 设置,则不生效。
#### find_process
!!! quote ""
仅支持 Linux、Windows 和 macOS。
在没有 `process_name``process_path``package_name``user``user_id` 规则时启用进程搜索以输出日志。
#### find_neighbor
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
在没有 `source_mac_address``source_hostname` 规则时启用邻居解析以输出日志。
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
#### dhcp_lease_files
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux 和 macOS。
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
为空时自动从常见 DHCP 服务器dnsmasq、odhcpd、ISC dhcpd、Kea检测。
#### default_domain_resolver
!!! question "自 sing-box 1.12.0 起"

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [package_name_regex](#package_name_regex)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [interface_address](#interface_address)
@@ -124,6 +130,9 @@ icon: material/new-box
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -159,6 +168,12 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -343,6 +358,12 @@ Match process path using regular expression.
Match android package name.
#### package_name_regex
!!! question "Since sing-box 1.14.0"
Match android package name using regular expression.
#### user
!!! quote ""
@@ -449,6 +470,26 @@ Match specified outbounds' preferred routes.
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
| `wireguard` | Match peers's allowed IPs |
#### source_mac_address
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device MAC address.
#### source_hostname
!!! question "Since sing-box 1.14.0"
!!! quote ""
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
Match source device hostname from DHCP leases.
#### rule_set
!!! question "Since sing-box 1.8.0"

View File

@@ -2,6 +2,12 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [source_mac_address](#source_mac_address)
:material-plus: [source_hostname](#source_hostname)
:material-plus: [package_name_regex](#package_name_regex)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [interface_address](#interface_address)
@@ -122,6 +128,9 @@ icon: material/new-box
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"user": [
"sekai"
],
@@ -157,6 +166,12 @@ icon: material/new-box
"tailscale",
"wireguard"
],
"source_mac_address": [
"00:11:22:33:44:55"
],
"source_hostname": [
"my-device"
],
"rule_set": [
"geoip-cn",
"geosite-cn"
@@ -341,6 +356,12 @@ icon: material/new-box
匹配 Android 应用包名。
#### package_name_regex
!!! question "自 sing-box 1.14.0 起"
使用正则表达式匹配 Android 应用包名。
#### user
!!! quote ""
@@ -447,6 +468,26 @@ icon: material/new-box
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
| `wireguard` | 匹配对端的 allowed IPs |
#### source_mac_address
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备 MAC 地址。
#### source_hostname
!!! question "自 sing-box 1.14.0 起"
!!! quote ""
仅支持 Linux、macOS或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
匹配源设备从 DHCP 租约获取的主机名。
#### rule_set
!!! question "自 sing-box 1.8.0 起"

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"
@@ -316,4 +327,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
Will overrides `dns.client_subnet`.
Will override `dns.client_subnet`.

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

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [query_type](#query_type)
!!! quote "Changes in sing-box 1.13.0"
:material-plus: [network_interface_address](#network_interface_address)
@@ -78,6 +83,9 @@ icon: material/new-box
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"network_type": [
"wifi"
],
@@ -125,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
@@ -205,6 +227,12 @@ Match process path using regular expression.
Match android package name.
#### package_name_regex
!!! question "Since sing-box 1.14.0"
Match android package name using regular expression.
#### network_type
!!! question "Since sing-box 1.11.0"

View File

@@ -2,6 +2,11 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [package_name_regex](#package_name_regex)
:material-alert: [query_type](#query_type)
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: [network_interface_address](#network_interface_address)
@@ -78,6 +83,9 @@ icon: material/new-box
"package_name": [
"com.termux"
],
"package_name_regex": [
"^com\\.termux.*"
],
"network_type": [
"wifi"
],
@@ -125,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
@@ -201,6 +220,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
匹配 Android 应用包名。
#### package_name_regex
!!! question "自 sing-box 1.14.0 起"
使用正则表达式匹配 Android 应用包名。
#### network_type
!!! question "自 sing-box 1.11.0 起"

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: version `5`
!!! quote "Changes in sing-box 1.13.0"
:material-plus: version `4`
@@ -41,6 +45,7 @@ Version of rule-set.
* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets.
* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items.
* 4: sing-box 1.13.0: Added `network_interface_address` and `default_interface_address` rule items.
* 5: sing-box 1.14.0: Added `package_name_regex` rule item.
#### rules

View File

@@ -2,6 +2,10 @@
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: version `5`
!!! quote "sing-box 1.13.0 中的更改"
:material-plus: version `4`
@@ -41,6 +45,7 @@ icon: material/new-box
* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。
* 3: sing-box 1.11.0: 添加了 `network_type``network_is_expensive``network_is_constrainted` 规则项。
* 4: sing-box 1.13.0: 添加了 `network_interface_address``default_interface_address` 规则项。
* 5: sing-box 1.14.0: 添加了 `package_name_regex` 规则项。
#### rules

View File

@@ -0,0 +1,150 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.14.0"
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [detour](#detour)
# ACME
!!! quote ""
`with_acme` build tag required.
### Structure
```json
{
"type": "acme",
"tag": "",
"domain": [],
"data_directory": "",
"default_server_name": "",
"email": "",
"provider": "",
"account_key": "",
"disable_http_challenge": false,
"disable_tls_alpn_challenge": false,
"alternative_http_port": 0,
"alternative_tls_port": 0,
"external_account": {
"key_id": "",
"mac_key": ""
},
"dns01_challenge": {},
"key_type": "",
"detour": ""
}
```
### Fields
#### domain
==Required==
List of domains.
#### data_directory
The directory to store ACME data.
`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty.
#### default_server_name
Server name to use when choosing a certificate if the ClientHello's ServerName field is empty.
#### email
The email address to use when creating or selecting an existing ACME server account.
#### provider
The ACME CA provider to use.
| Value | Provider |
|-------------------------|---------------|
| `letsencrypt (default)` | Let's Encrypt |
| `zerossl` | ZeroSSL |
| `https://...` | Custom |
When `provider` is `zerossl`, sing-box will automatically request ZeroSSL EAB credentials if `email` is set and
`external_account` is empty.
When `provider` is `zerossl`, at least one of `external_account`, `email`, or `account_key` is required.
#### account_key
!!! question "Since sing-box 1.14.0"
The PEM-encoded private key of an existing ACME account.
#### disable_http_challenge
Disable all HTTP challenges.
#### disable_tls_alpn_challenge
Disable all TLS-ALPN challenges
#### alternative_http_port
The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a
listener for the HTTP challenge.
#### alternative_tls_port
The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to
succeed.
#### external_account
EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known
by the CA.
External account bindings are used to associate an ACME account with an existing account in a non-ACME system, such as
a CA customer database.
To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a
key identifier, using some mechanism outside of ACME. §7.3.4
#### external_account.key_id
The key identifier.
#### external_account.mac_key
The MAC key.
#### dns01_challenge
ACME DNS01 challenge field. If configured, other challenge methods will be disabled.
See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details.
#### key_type
!!! question "Since sing-box 1.14.0"
The private key type to generate for new certificates.
| Value | Type |
|------------|---------|
| `ed25519` | Ed25519 |
| `p256` | P-256 |
| `p384` | P-384 |
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### detour
!!! question "Since sing-box 1.14.0"
The tag of the upstream outbound.
All provider HTTP requests will use this outbound.

View File

@@ -0,0 +1,145 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.14.0 中的更改"
:material-plus: [account_key](#account_key)
:material-plus: [key_type](#key_type)
:material-plus: [detour](#detour)
# ACME
!!! quote ""
需要 `with_acme` 构建标签。
### 结构
```json
{
"type": "acme",
"tag": "",
"domain": [],
"data_directory": "",
"default_server_name": "",
"email": "",
"provider": "",
"account_key": "",
"disable_http_challenge": false,
"disable_tls_alpn_challenge": false,
"alternative_http_port": 0,
"alternative_tls_port": 0,
"external_account": {
"key_id": "",
"mac_key": ""
},
"dns01_challenge": {},
"key_type": "",
"detour": ""
}
```
### 字段
#### domain
==必填==
域名列表。
#### data_directory
ACME 数据存储目录。
如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`
#### default_server_name
如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。
#### email
创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。
#### provider
要使用的 ACME CA 提供商。
| 值 | 提供商 |
|--------------------|---------------|
| `letsencrypt (默认)` | Let's Encrypt |
| `zerossl` | ZeroSSL |
| `https://...` | 自定义 |
`provider``zerossl` 时,如果设置了 `email` 且未设置 `external_account`
sing-box 会自动向 ZeroSSL 请求 EAB 凭据。
`provider``zerossl` 时,必须至少设置 `external_account``email``account_key` 之一。
#### account_key
!!! question "自 sing-box 1.14.0 起"
现有 ACME 帐户的 PEM 编码私钥。
#### disable_http_challenge
禁用所有 HTTP 质询。
#### disable_tls_alpn_challenge
禁用所有 TLS-ALPN 质询。
#### alternative_http_port
用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。
#### alternative_tls_port
用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。
#### external_account
EAB外部帐户绑定包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。
外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。
为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4
#### external_account.key_id
密钥标识符。
#### external_account.mac_key
MAC 密钥。
#### dns01_challenge
ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
参阅 [DNS01 质询字段](/zh/configuration/shared/dns01_challenge/)。
#### key_type
!!! question "自 sing-box 1.14.0 起"
为新证书生成的私钥类型。
| 值 | 类型 |
|-----------|----------|
| `ed25519` | Ed25519 |
| `p256` | P-256 |
| `p384` | P-384 |
| `rsa2048` | RSA |
| `rsa4096` | RSA |
#### detour
!!! question "自 sing-box 1.14.0 起"
上游出站的标签。
所有提供者 HTTP 请求将使用此出站。

View File

@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.14.0"
# Cloudflare Origin CA
### Structure
```json
{
"type": "cloudflare-origin-ca",
"tag": "",
"domain": [],
"data_directory": "",
"api_token": "",
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"detour": ""
}
```
### Fields
#### domain
==Required==
List of domain names or wildcard domain names to include in the certificate.
#### data_directory
Root directory used to store the issued certificate, private key, and metadata.
If empty, sing-box uses the same default data directory as the ACME certificate provider:
`$XDG_DATA_HOME/certmagic` or `$HOME/.local/share/certmagic`.
#### api_token
Cloudflare API token used to create the certificate.
Get or create one in [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens).
Requires the `Zone / SSL and Certificates / Edit` permission.
Conflict with `origin_ca_key`.
#### origin_ca_key
Cloudflare Origin CA Key.
Get it in [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens).
Conflict with `api_token`.
#### request_type
The signature type to request from Cloudflare.
| Value | Type |
|----------------------|-------------|
| `origin-rsa` | RSA |
| `origin-ecc` | ECDSA P-256 |
`origin-rsa` is used if empty.
#### requested_validity
The requested certificate validity in days.
Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
`5475` days (15 years) is used if empty.
#### detour
The tag of the upstream outbound.
All provider HTTP requests will use this outbound.

View File

@@ -0,0 +1,82 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.14.0 起"
# Cloudflare Origin CA
### 结构
```json
{
"type": "cloudflare-origin-ca",
"tag": "",
"domain": [],
"data_directory": "",
"api_token": "",
"origin_ca_key": "",
"request_type": "",
"requested_validity": 0,
"detour": ""
}
```
### 字段
#### domain
==必填==
要写入证书的域名或通配符域名列表。
#### data_directory
保存签发证书、私钥和元数据的根目录。
如果为空sing-box 会使用与 ACME 证书提供者相同的默认数据目录:
`$XDG_DATA_HOME/certmagic``$HOME/.local/share/certmagic`
#### api_token
用于创建证书的 Cloudflare API Token。
可在 [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens) 获取或创建。
需要 `Zone / SSL and Certificates / Edit` 权限。
`origin_ca_key` 冲突。
#### origin_ca_key
Cloudflare Origin CA Key。
可在 [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens) 获取。
`api_token` 冲突。
#### request_type
向 Cloudflare 请求的签名类型。
| 值 | 类型 |
|----------------------|-------------|
| `origin-rsa` | RSA |
| `origin-ecc` | ECDSA P-256 |
如果为空,使用 `origin-rsa`
#### requested_validity
请求的证书有效期,单位为天。
可用值:`7``30``90``365``730``1095``5475`
如果为空,使用 `5475`15 年)。
#### detour
上游出站的标签。
所有提供者 HTTP 请求将使用此出站。

Some files were not shown because too many files have changed in this diff Show More