Merge tag 'v1.14.3'

This commit is contained in:
Shtorm
2026-10-19 01:05:55 +03:00
321 changed files with 6750 additions and 5105 deletions
+6 -5
View File
@@ -525,7 +525,7 @@ jobs:
if [[ "${{ matrix.legacy_osx }}" != "true" ]]; then
TAGS=$(cat release/DEFAULT_BUILD_TAGS)
else
TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS)
TAGS=$(sed 's/,with_usbip//' release/DEFAULT_BUILD_TAGS_OTHERS)
fi
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Set shared ldflags
@@ -1214,7 +1214,7 @@ jobs:
path: 'dist'
build_apple_library:
name: Build Apple library (${{ matrix.artifact }})
runs-on: ${{ matrix.build && 'macos-26' || 'ubuntu-latest' }}
runs-on: ${{ matrix.build && 'xcode-27' || 'ubuntu-latest' }}
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'Apple-release' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
needs:
- calculate_version
@@ -1285,7 +1285,7 @@ jobs:
compression-level: 1
build_apple:
name: Build Apple release clients
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Apple-release' || inputs.build == 'macOS-standalone' || inputs.build == 'iOS-jailbreak'
needs:
- calculate_version
@@ -1559,7 +1559,7 @@ jobs:
--api-key $ASC_KEY_ID \
--api-issuer 00000000-0000-0000-0000-000000000000
- name: Publish to TestFlight
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/testing'
if: matrix.if && matrix.type == 'app-store' && github.event_name == 'workflow_dispatch'
run: |-
arguments=(publish_testflight "${{ matrix.platform }}")
if [[ -n "$TEST_MESSAGE" ]]; then
@@ -1596,6 +1596,7 @@ jobs:
make -C clients/apple build_ios_deb
mkdir -p dist
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64.deb" dist
cp "clients/apple/build/jailbreak/SFI-${VERSION}-iphoneos-arm64e.deb" dist
env:
XCODEBUILD_CLONED_SOURCE_PACKAGES_DIR_PATH: ${{ runner.temp }}/SourcePackages
- name: Report cache directory sizes
@@ -1616,7 +1617,7 @@ jobs:
path: 'dist'
build_apple_app_store:
name: Build Apple App Store clients
runs-on: ${{ matrix.if && 'macos-26' || 'ubuntu-latest' }}
runs-on: ${{ matrix.if && 'xcode-27' || 'ubuntu-latest' }}
if: github.event_name == 'workflow_dispatch' && (inputs.build == 'All' || inputs.build == 'Apple' || inputs.build == 'iOS' || inputs.build == 'macOS' || inputs.build == 'tvOS')
needs:
- calculate_version
+7 -4
View File
@@ -174,6 +174,7 @@ build_ios_deb:
upload_ios_deb:
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64.deb"
ghr --replace --draft --prerelease "v${VERSION}" ../sing-box-for-apple/build/jailbreak/"SFI-${VERSION}-iphoneos-arm64e.deb"
release_ios: build_ios upload_ios_app_store
@@ -273,7 +274,7 @@ update_apple_version:
go run ./cmd/internal/update_apple_version
update_macos_version:
MACOS_PROJECT_VERSION=$(shell go run -v ./cmd/internal/app_store_connect next_macos_project_version) go run ./cmd/internal/update_apple_version
MACOS_PROJECT_VERSION=$(shell go run ./cmd/internal/app_store_connect next_project_version macos) go run ./cmd/internal/update_apple_version
release_apple: lib_apple update_apple_version release_ios release_macos release_tvos release_macos_standalone
@@ -282,11 +283,13 @@ release_apple_beta: update_apple_version release_ios release_macos release_tvos
publish_testflight:
go run -v ./cmd/internal/app_store_connect publish_testflight $(filter-out $@,$(MAKECMDGOALS))
prepare_app_store:
go run -v ./cmd/internal/app_store_connect prepare_app_store
submit_app_store:
go run -v ./cmd/internal/app_store_connect submit_app_store $(filter-out $@,$(MAKECMDGOALS))
release_app_store: release_ios release_tvos submit_app_store
publish_app_store:
go run -v ./cmd/internal/app_store_connect publish_app_store
go run -v ./cmd/internal/app_store_connect publish_app_store $(filter-out $@,$(MAKECMDGOALS))
test:
@go test -v ./... && \
+7 -96
View File
@@ -2,86 +2,43 @@ 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 {
func NewManager(registry adapter.CertificateProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.CertificateProviderService),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) 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)
err := scope.Start(name, provider, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return err
}
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()
@@ -95,29 +52,6 @@ func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
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 {
@@ -125,32 +59,9 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
}
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:]...)
_, loaded := m.providerByTag[tag]
if loaded {
return E.New("duplicate certificate provider tag: ", tag)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
-1
View File
@@ -33,6 +33,5 @@ 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
}
-23
View File
@@ -8,9 +8,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
@@ -45,26 +43,6 @@ type DNSQueryOptions struct {
RemoveClientSubnet bool
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (DNSQueryOptions, error) {
if options == nil || options.Server == "" {
return DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return DNSQueryOptions{}, E.New("domain resolver not found: " + options.Server)
}
return DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
DisableOptimisticCache: options.DisableOptimisticCache,
RewriteTTL: options.RewriteTTL,
Timeout: time.Duration(options.Timeout),
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
}
type RDRCStore interface {
LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
SaveRDRC(transportName string, qName string, qType uint16) error
@@ -111,6 +89,5 @@ type DNSTransportManager interface {
Transport(tag string) (DNSTransport, bool)
Default() DNSTransport
FakeIP() FakeIPTransport
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error
}
+1 -1
View File
@@ -21,8 +21,8 @@ type EndpointRegistry interface {
type EndpointManager interface {
Lifecycle
StartEndpoint(endpoint Endpoint) error
Endpoints() []Endpoint
Get(tag string) (Endpoint, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error
}
+12 -89
View File
@@ -2,46 +2,36 @@ package endpoint
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.EndpointManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.EndpointRegistry
access sync.Mutex
started bool
stage adapter.StartStage
scope *adapter.Scope
endpoints []adapter.Endpoint
endpointByTag map[string]adapter.Endpoint
}
func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager {
func NewManager(registry adapter.EndpointRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpointByTag: make(map[string]adapter.Endpoint),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
if stage == adapter.StartStateInitialize {
m.scope = scope
}
m.started = true
m.stage = stage
if stage == adapter.StartStateStart {
// started with outbound manager
m.access.Unlock()
return nil
}
@@ -49,38 +39,16 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Unlock()
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(endpoint, stage)
done()
err := scope.Start(name, endpoint, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return err
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
endpoints := m.endpoints
m.endpoints = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, endpoint := range endpoints {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, endpoint.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
func (m *Manager) StartEndpoint(endpoint adapter.Endpoint) error {
return m.scope.Start("endpoint/"+endpoint.Type()+"["+endpoint.Tag()+"]", endpoint, adapter.StartStateStart)
}
func (m *Manager) Endpoints() []adapter.Endpoint {
@@ -96,29 +64,6 @@ func (m *Manager) Get(tag string) (adapter.Endpoint, bool) {
return endpoint, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
endpoint, found := m.endpointByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.endpointByTag, tag)
index := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == endpoint
})
if index == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return endpoint.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
@@ -126,31 +71,9 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "endpoint/" + endpoint.Type() + "[" + endpoint.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(endpoint, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsEndpoint, loaded := m.endpointByTag[tag]; loaded {
if m.started {
err = existsEndpoint.Close()
if err != nil {
return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]")
}
}
existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool {
return it == existsEndpoint
})
if existsIndex == -1 {
panic("invalid endpoint index")
}
m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...)
_, loaded := m.endpointByTag[tag]
if loaded {
return E.New("duplicate endpoint tag: ", tag)
}
m.endpoints = append(m.endpoints, endpoint)
m.endpointByTag[tag] = endpoint
+10 -1
View File
@@ -41,7 +41,6 @@ type InboundManager interface {
Lifecycle
Inbounds() []Inbound
Get(tag string) (Inbound, bool)
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error
}
@@ -191,6 +190,16 @@ func DNSTransportTagFromContext(ctx context.Context) (string, bool) {
return transportTag, loaded
}
func ContextForMultiplexSession(ctx context.Context) context.Context {
var sessionContext InboundContext
metadata := ContextFrom(ctx)
if metadata != nil {
sessionContext.Outbound = metadata.Outbound
}
ctx = ContextWithDNSTransportTag(ctx, "")
return WithContext(ctx, &sessionContext)
}
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
return context.WithValue(ctx, (*inboundContextKey)(nil), inboundContext)
}
+7 -91
View File
@@ -2,84 +2,45 @@ package inbound
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.InboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.InboundRegistry
endpoint adapter.EndpointManager
access sync.Mutex
started bool
stage adapter.StartStage
inbounds []adapter.Inbound
inboundByTag map[string]adapter.Inbound
}
func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
func NewManager(registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
inboundByTag: make(map[string]adapter.Inbound),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(inbound, stage)
done()
err := scope.Start(name, inbound, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return err
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
inbounds := m.inbounds
m.inbounds = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, inbound := range inbounds {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, inbound.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Inbounds() []adapter.Inbound {
m.access.Lock()
defer m.access.Unlock()
@@ -96,29 +57,6 @@ func (m *Manager) Get(tag string) (adapter.Inbound, bool) {
return m.endpoint.Get(tag)
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
inbound, found := m.inboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.inboundByTag, tag)
index := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == inbound
})
if index == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return inbound.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error {
inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options)
if err != nil {
@@ -126,31 +64,9 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "inbound/" + inbound.Type() + "[" + inbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(inbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsInbound, loaded := m.inboundByTag[tag]; loaded {
if m.started {
err = existsInbound.Close()
if err != nil {
return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]")
}
}
existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool {
return it == existsInbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...)
_, loaded := m.inboundByTag[tag]
if loaded {
return E.New("duplicate inbound tag: ", tag)
}
m.inbounds = append(m.inbounds, inbound)
m.inboundByTag[tag] = inbound
+98 -46
View File
@@ -2,13 +2,15 @@ package adapter
import (
"context"
"reflect"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/x/list"
)
type SimpleLifecycle interface {
@@ -48,8 +50,7 @@ func (s StartStage) String() string {
}
type Lifecycle interface {
Start(stage StartStage) error
Close() error
Start(stage StartStage, scope *Scope) error
}
type LifecycleService interface {
@@ -57,55 +58,106 @@ type LifecycleService interface {
Lifecycle
}
func getServiceName(service any) string {
if named, ok := service.(interface {
Type() string
Tag() string
}); ok {
tag := named.Tag()
if tag != "" {
return named.Type() + "[" + tag + "]"
}
return named.Type()
}
t := reflect.TypeOf(service)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
return strings.ToLower(t.Name())
type Scope struct {
ctx context.Context
cancel context.CancelFunc
logger log.ContextLogger
access sync.Mutex
cleanups list.List[func() error]
children map[Lifecycle]*scopeChild
}
func Start(ctx context.Context, logger log.ContextLogger, stage StartStage, services ...Lifecycle) error {
for _, service := range services {
err := ctx.Err()
if err != nil {
return err
}
name := getServiceName(service)
done := LogElapsed(logger, stage, " ", name)
err = service.Start(stage)
done()
if err != nil {
return err
}
type scopeChild struct {
scope *Scope
cleanup *list.Element[func() error]
}
func NewScope(ctx context.Context, logger log.ContextLogger) *Scope {
ctx, cancel := context.WithCancel(ctx)
return &Scope{
ctx: ctx,
cancel: cancel,
logger: logger,
children: make(map[Lifecycle]*scopeChild),
}
}
func (s *Scope) Context() context.Context {
return s.ctx
}
func (s *Scope) Add(cleanup func() error) {
s.access.Lock()
s.cleanups.PushBack(cleanup)
s.access.Unlock()
}
func (s *Scope) Start(name string, component Lifecycle, stage StartStage) error {
s.access.Lock()
err := s.ctx.Err()
if err != nil {
s.access.Unlock()
return err
}
child, loaded := s.children[component]
if !loaded {
childScope := NewScope(s.ctx, s.logger)
child = &scopeChild{scope: childScope}
child.cleanup = s.cleanups.PushBack(func() error {
done := LogElapsed(s.logger, "close ", name)
monitor := taskmonitor.New(s.logger, C.StopTimeout)
monitor.Start("close ", name)
closeErr := childScope.Close()
monitor.Finish()
done()
if closeErr != nil {
return E.Cause(closeErr, "close ", name)
}
return nil
})
s.children[component] = child
}
s.access.Unlock()
done := LogElapsed(s.logger, stage, " ", name)
monitor := taskmonitor.New(s.logger, C.StartTimeout)
monitor.Start(stage, " ", name)
err = component.Start(stage, child.scope)
monitor.Finish()
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
return nil
}
func StartNamed(ctx context.Context, logger log.ContextLogger, stage StartStage, services []LifecycleService) error {
for _, service := range services {
err := ctx.Err()
if err != nil {
return err
}
done := LogElapsed(logger, stage, " ", service.Name())
err = service.Start(stage)
done()
if err != nil {
return E.Cause(err, stage.String(), " ", service.Name())
}
func (s *Scope) Remove(component Lifecycle) error {
s.access.Lock()
child, loaded := s.children[component]
if !loaded {
s.access.Unlock()
return nil
}
return nil
delete(s.children, component)
cleanup := s.cleanups.Remove(child.cleanup)
s.access.Unlock()
return cleanup()
}
func (s *Scope) Close() error {
s.access.Lock()
s.cancel()
var cleanups []func() error
for element := s.cleanups.Back(); element != nil; element = element.Prev() {
cleanups = append(cleanups, element.Value)
}
s.cleanups.Init()
s.children = nil
s.access.Unlock()
var err error
for _, cleanup := range cleanups {
err = E.Errors(err, cleanup())
}
return err
}
func LogElapsed(logger log.ContextLogger, description ...any) func() {
-52
View File
@@ -1,52 +0,0 @@
package adapter
func LegacyStart(starter any, stage StartStage) error {
if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle {
return lifecycle.Start(stage)
}
switch stage {
case StartStateInitialize:
if preStarter, isPreStarter := starter.(interface {
PreStart() error
}); isPreStarter {
return preStarter.PreStart()
}
case StartStateStart:
if starter, isStarter := starter.(interface {
Start() error
}); isStarter {
return starter.Start()
}
case StartStateStarted:
if postStarter, isPostStarter := starter.(interface {
PostStart() error
}); isPostStarter {
return postStarter.PostStart()
}
}
return nil
}
type lifecycleServiceWrapper struct {
SimpleLifecycle
name string
}
func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{
SimpleLifecycle: service,
name: name,
}
}
func (l *lifecycleServiceWrapper) Name() string {
return l.name
}
func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.SimpleLifecycle, stage)
}
func (l *lifecycleServiceWrapper) Close() error {
return l.SimpleLifecycle.Close()
}
+9
View File
@@ -51,3 +51,12 @@ type OutboundManager interface {
Remove(tag string) error
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error
}
type IdleConnectionKeeper interface {
SetKeepIdleConnections(keep bool)
CloseIdleConnections()
}
type Referrer interface {
References() []string
}
+103 -133
View File
@@ -2,29 +2,25 @@ package outbound
import (
"context"
"io"
"os"
"strings"
"sync"
"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"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.OutboundManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.OutboundRegistry
endpoint adapter.EndpointManager
defaultTag string
access sync.RWMutex
started bool
startAccess sync.Mutex
scope *adapter.Scope
stage adapter.StartStage
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
@@ -33,9 +29,8 @@ type Manager struct {
defaultOutboundFallback func() (adapter.Outbound, error)
}
func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
func NewManager(registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager {
return &Manager{
logger: logger,
registry: registry,
endpoint: endpoint,
defaultTag: defaultTag,
@@ -48,14 +43,13 @@ func (m *Manager) Initialize(defaultOutboundFallback func() (adapter.Outbound, e
m.defaultOutboundFallback = defaultOutboundFallback
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.scope = scope
m.stage = stage
if stage == adapter.StartStateStart {
if stage == adapter.StartStateInitialize {
if m.defaultTag != "" && m.defaultOutbound == nil {
defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag)
if !loaded {
@@ -74,27 +68,27 @@ func (m *Manager) Start(stage adapter.StartStage) error {
m.outboundByTag[directOutbound.Tag()] = directOutbound
m.defaultOutbound = directOutbound
}
outbounds := m.outbounds
m.access.Unlock()
return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
} else {
outbounds := m.outbounds
m.access.Unlock()
for _, outbound := range outbounds {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(outbound, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
outbounds := m.outbounds
m.access.Unlock()
if stage == adapter.StartStateStart {
return m.startOutbounds(scope, append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...))
}
for _, outbound := range outbounds {
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
if !isLifecycle {
continue
}
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
err := scope.Start(name, lifecycle, stage)
if err != nil {
return err
}
}
return nil
}
func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
func (m *Manager) startOutbounds(scope *adapter.Scope, outbounds []adapter.Outbound) error {
started := make(map[string]bool)
for {
canContinue := false
@@ -112,27 +106,21 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
}
started[outboundTag] = true
canContinue = true
if endpoint, isEndpoint := outboundToStart.(adapter.Endpoint); isEndpoint {
err := m.endpoint.StartEndpoint(endpoint)
if err != nil {
return err
}
continue
}
lifecycle, isLifecycle := outboundToStart.(adapter.Lifecycle)
if !isLifecycle {
continue
}
name := "outbound/" + outboundToStart.Type() + "[" + outboundTag + "]"
if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
} else if starter, isStarter := outboundToStart.(interface {
Start() error
}); isStarter {
done := adapter.LogElapsed(m.logger, "start ", name)
monitor.Start("start ", name)
err := starter.Start()
monitor.Finish()
done()
if err != nil {
return E.Cause(err, "start ", name)
}
err := scope.Start(name, lifecycle, adapter.StartStateStart)
if err != nil {
return err
}
}
if len(started) == len(outbounds) {
@@ -152,9 +140,9 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
if common.Contains(oTree, problemOutboundTag) {
return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag)
}
m.access.Lock()
problemOutbound := m.outboundByTag[problemOutboundTag]
m.access.Unlock()
problemOutbound := common.Find(outbounds, func(it adapter.Outbound) bool {
return it.Tag() == problemOutboundTag
})
if problemOutbound == nil {
return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]")
}
@@ -165,33 +153,6 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error {
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
outbounds := m.outbounds
m.outbounds = nil
m.access.Unlock()
var err error
for _, outbound := range outbounds {
if closer, isCloser := outbound.(io.Closer); isCloser {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
}
return nil
}
func (m *Manager) Outbounds() []adapter.Outbound {
m.access.RLock()
defer m.access.RUnlock()
@@ -215,47 +176,38 @@ func (m *Manager) Default() adapter.Outbound {
}
func (m *Manager) Remove(tag string) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
defer m.access.Unlock()
outbound, found := m.outboundByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.outboundByTag, tag)
index := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == outbound
})
if index == -1 {
panic("invalid inbound index")
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
m.access.Unlock()
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...)
started := m.started
delete(m.outboundByTag, tag)
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
return it != outbound
})
m.removeDependencies(outbound)
if m.defaultOutbound == outbound {
if len(m.outbounds) > 0 {
m.defaultOutbound = m.outbounds[0]
m.logger.Notice("updated default outbound to ", m.defaultOutbound.Tag())
} else {
m.defaultOutbound = nil
}
}
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", "))
scope := m.scope
m.access.Unlock()
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
if scope == nil || !isLifecycle {
return nil
}
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
if started {
return common.Close(outbound)
}
return nil
return scope.Remove(lifecycle)
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error {
@@ -266,45 +218,63 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.
if err != nil {
return err
}
if m.started {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.RLock()
scope := m.scope
stage := m.stage
m.access.RUnlock()
lifecycle, isLifecycle := outbound.(adapter.Lifecycle)
if scope != nil && isLifecycle {
name := "outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(outbound, stage)
done()
for _, startStage := range adapter.ListStartStages[:stage+1] {
err = scope.Start(name, lifecycle, startStage)
if err != nil {
return E.Cause(err, stage, " ", name)
return E.Errors(err, scope.Remove(lifecycle))
}
}
}
m.access.Lock()
defer m.access.Unlock()
if existsOutbound, loaded := m.outboundByTag[tag]; loaded {
if m.started {
err = common.Close(existsOutbound)
if err != nil {
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
}
}
existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool {
return it == existsOutbound
existsOutbound, loaded := m.outboundByTag[tag]
if loaded {
m.outbounds = common.Filter(m.outbounds, func(it adapter.Outbound) bool {
return it != existsOutbound
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...)
m.removeDependencies(existsOutbound)
}
m.outbounds = append(m.outbounds, outbound)
m.outboundByTag[tag] = outbound
dependencies := outbound.Dependencies()
for _, dependency := range dependencies {
for _, dependency := range outbound.Dependencies() {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) {
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) || (loaded && m.defaultOutbound == existsOutbound) {
m.defaultOutbound = outbound
if m.started {
m.logger.Notice("updated default outbound to ", outbound.Tag())
}
}
m.access.Unlock()
if !loaded || scope == nil {
return nil
}
existsLifecycle, isExistsLifecycle := existsOutbound.(adapter.Lifecycle)
if !isExistsLifecycle {
return nil
}
err = scope.Remove(existsLifecycle)
if err != nil {
return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]")
}
return nil
}
func (m *Manager) removeDependencies(outbound adapter.Outbound) {
tag := outbound.Tag()
for _, dependency := range outbound.Dependencies() {
dependBy := common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
if len(dependBy) == 0 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = dependBy
}
}
}
+1
View File
@@ -10,6 +10,7 @@ import (
)
type Provider interface {
Lifecycle
Type() string
Tag() string
Outbounds() []Outbound
+40 -72
View File
@@ -2,80 +2,46 @@ package provider
import (
"context"
"io"
"os"
"sync"
"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"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.ProviderManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ProviderRegistry
access sync.Mutex
started bool
startAccess sync.Mutex
scope *adapter.Scope
stage adapter.StartStage
providers []adapter.Provider
providerByTag map[string]adapter.Provider
wg sync.WaitGroup
}
func NewManager(logger logger.ContextLogger, registry adapter.ProviderRegistry) *Manager {
func NewManager(registry adapter.ProviderRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
providerByTag: make(map[string]adapter.Provider),
}
}
func (m *Manager) Initialize() {
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.scope = scope
m.stage = stage
providers := m.providers
m.access.Unlock()
for _, provider := range providers {
err := adapter.LegacyStart(provider, stage)
err := scope.Start("provider/"+provider.Type()+"["+provider.Tag()+"]", provider, stage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
providers := m.providers
m.providers = nil
m.access.Unlock()
var err error
for _, provider := range providers {
if closer, isCloser := provider.(io.Closer); isCloser {
monitor.Start("close provider/", provider.Type(), "[", provider.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close provider/", provider.Type(), "[", provider.Tag(), "]")
})
monitor.Finish()
return err
}
}
return nil
@@ -95,6 +61,8 @@ func (m *Manager) Get(tag string) (adapter.Provider, bool) {
}
func (m *Manager) Remove(tag string) error {
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
provider, found := m.providerByTag[tag]
if !found {
@@ -102,56 +70,56 @@ func (m *Manager) Remove(tag string) error {
return os.ErrInvalid
}
delete(m.providerByTag, tag)
index := common.Index(m.providers, func(it adapter.Provider) bool {
return it == provider
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
return it != provider
})
if index == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:index], m.providers[index+1:]...)
started := m.started
scope := m.scope
m.access.Unlock()
if started {
return common.Close(provider)
if scope == nil {
return nil
}
return nil
return scope.Remove(provider)
}
func (m *Manager) Create(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, providerType string, options any) error {
if tag == "" {
return os.ErrInvalid
}
provider, err := m.registry.CreateProvider(ctx, router, logFactory, tag, providerType, options)
if err != nil {
return err
}
m.startAccess.Lock()
defer m.startAccess.Unlock()
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(provider, stage)
scope := m.scope
stage := m.stage
m.access.Unlock()
if scope != nil {
name := "provider/" + provider.Type() + "[" + provider.Tag() + "]"
for _, startStage := range adapter.ListStartStages[:stage+1] {
err = scope.Start(name, provider, startStage)
if err != nil {
return E.Cause(err, stage, " provider/", provider.Type(), "[", provider.Tag(), "]")
return E.Errors(err, scope.Remove(provider))
}
}
}
if existsProvider, loaded := m.providerByTag[tag]; loaded {
if m.started {
err = common.Close(existsProvider)
if err != nil {
return E.Cause(err, "close provider", provider.Type(), "[", existsProvider.Tag(), "]")
}
}
existsIndex := common.Index(m.providers, func(it adapter.Provider) bool {
return it == existsProvider
m.access.Lock()
existsProvider, loaded := m.providerByTag[tag]
if loaded {
m.providers = common.Filter(m.providers, func(it adapter.Provider) bool {
return it != existsProvider
})
if existsIndex == -1 {
panic("invalid provider index")
}
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
}
m.providers = append(m.providers, provider)
m.providerByTag[tag] = provider
m.access.Unlock()
if !loaded || scope == nil {
return nil
}
err = scope.Remove(existsProvider)
if err != nil {
return E.Cause(err, "close provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
}
return nil
}
-1
View File
@@ -22,6 +22,5 @@ type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
}
+7 -91
View File
@@ -2,82 +2,43 @@ package service
import (
"context"
"os"
"sync"
"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"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
func NewManager(registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, stage, " ", name)
err := adapter.LegacyStart(service, stage)
done()
err := scope.Start(name, service, stage)
if err != nil {
return E.Cause(err, stage, " ", name)
return err
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
done := adapter.LogElapsed(m.logger, "close ", name)
monitor.Start("close ", name)
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close ", name)
})
monitor.Finish()
done()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
@@ -91,29 +52,6 @@ func (m *Manager) Get(tag string) (adapter.Service, bool) {
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
@@ -121,31 +59,9 @@ func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag stri
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
name := "service/" + service.Type() + "[" + service.Tag() + "]"
for _, stage := range adapter.ListStartStages {
done := adapter.LogElapsed(m.logger, stage, " ", name)
err = adapter.LegacyStart(service, stage)
done()
if err != nil {
return E.Cause(err, stage, " ", name)
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
_, loaded := m.serviceByTag[tag]
if loaded {
return E.New("duplicate service tag: ", tag)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
+14
View File
@@ -0,0 +1,14 @@
package adapter
import "context"
type keepSessionKey struct{}
func ContextWithKeepSession(ctx context.Context) context.Context {
return context.WithValue(ctx, (*keepSessionKey)(nil), true)
}
func KeepSessionFromContext(ctx context.Context) bool {
keep, _ := ctx.Value((*keepSessionKey)(nil)).(bool)
return keep
}
+5
View File
@@ -22,3 +22,8 @@ type V2RayClientTransport interface {
DialContext(ctx context.Context) (net.Conn, error)
Close() error
}
type V2RayMultiplexClientTransport interface {
V2RayClientTransport
MultiplexEnabled() bool
}
+127 -120
View File
@@ -2,11 +2,7 @@ package box
import (
"context"
"fmt"
"io"
"net/http"
"os"
"runtime/debug"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -45,10 +41,8 @@ import (
var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct {
ctx context.Context
createdAt time.Time
debugOptions option.DebugOptions
debugHTTPServer *http.Server
logFactory log.Factory
logger log.ContextLogger
network *route.NetworkManager
@@ -62,9 +56,11 @@ type Box struct {
dnsRouter *dns.Router
connection *route.ConnectionManager
router *route.Router
referenceManager *route.ReferenceManager
httpClientService adapter.LifecycleService
internalService []adapter.LifecycleService
done chan struct{}
ntpService *ntp.Service
scope *adapter.Scope
}
type Options struct {
@@ -224,13 +220,13 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.NetworkNamespaceManager](ctx, netnsManager)
internalServices = append(internalServices, netnsManager)
dnsOptions := common.PtrValueOrDefault(options.DNS)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
providerManager := provider.NewManager(logFactory.NewLogger("provider"), providerRegistry)
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)
endpointManager := endpoint.NewManager(endpointRegistry)
inboundManager := inbound.NewManager(inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(outboundRegistry, endpointManager, routeOptions.Final)
providerManager := provider.NewManager(providerRegistry)
dnsTransportManager := dns.NewTransportManager(dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(serviceRegistry)
certificateProviderManager := boxCertificate.NewManager(certificateProviderRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
@@ -244,13 +240,13 @@ func New(options Options) (*Box, error) {
}
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
if err != nil {
return nil, E.Cause(err, "initialize network manager")
}
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
// Must register after ConnectionManager: the Apple HTTP engine's proxy bridge reads it from the context when Manager.Start resolves the default client.
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
@@ -274,6 +270,8 @@ func New(options Options) (*Box, error) {
service.MustRegisterPtr(ctx, clashMode)
internalServices = append(internalServices, clashMode)
}
referenceManager := route.NewReferenceManager(ctx, logFactory.NewLogger("reference"), options.Options)
internalServices = append(internalServices, referenceManager)
ntpOptions := common.PtrValueOrDefault(options.NTP)
var timeService *tls.TimeServiceWrapper
if ntpOptions.Enabled {
@@ -485,6 +483,7 @@ func New(options Options) (*Box, error) {
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
}
}
var ntpService *ntp.Service
if ntpOptions.Enabled {
if ntpOptions.WriteToSystem {
err = adapter.CheckSecurityFeature(ctx, "NTP `write_to_system`")
@@ -496,7 +495,7 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, E.Cause(err, "create NTP service")
}
ntpService := ntp.NewService(ntp.Options{
ntpService = ntp.NewService(ntp.Options{
Context: ctx,
Dialer: ntpDialer,
Logger: logFactory.NewLogger("ntp"),
@@ -505,10 +504,8 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem,
})
timeService.TimeService = ntpService
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
}
return &Box{
ctx: ctx,
network: networkManager,
endpoint: endpointManager,
inbound: inboundManager,
@@ -520,28 +517,21 @@ func New(options Options) (*Box, error) {
dnsRouter: dnsRouter,
connection: connectionManager,
router: router,
referenceManager: referenceManager,
httpClientService: httpClientService,
createdAt: createdAt,
debugOptions: debugOptions,
logFactory: logFactory,
logger: logFactory.Logger(),
internalService: internalServices,
done: make(chan struct{}),
ntpService: ntpService,
scope: adapter.NewScope(ctx, logFactory.Logger()),
}, nil
}
func (s *Box) PreStart() error {
err := s.preStart()
if err != nil {
// TODO: remove catch error
defer func() {
v := recover()
if v != nil {
println(err.Error())
debug.PrintStack()
panic("panic on early close: " + fmt.Sprint(v))
}
}()
s.Close()
return err
}
@@ -552,15 +542,6 @@ func (s *Box) PreStart() error {
func (s *Box) Start() error {
err := s.start()
if err != nil {
// TODO: remove catch error
defer func() {
v := recover()
if v != nil {
println(err.Error())
debug.PrintStack()
println("panic on early start: " + fmt.Sprint(v))
}
}()
s.Close()
return err
}
@@ -568,6 +549,31 @@ func (s *Box) Start() error {
return nil
}
type boxComponent struct {
name string
lifecycle adapter.Lifecycle
}
func (s *Box) startComponents(stage adapter.StartStage, components ...boxComponent) error {
for _, component := range components {
err := s.scope.Start(component.name, component.lifecycle, stage)
if err != nil {
return err
}
}
return nil
}
func (s *Box) startInternalServices(stage adapter.StartStage) error {
for _, lifecycleService := range s.internalService {
err := s.scope.Start(lifecycleService.Name(), lifecycleService, stage)
if err != nil {
return err
}
}
return nil
}
func (s *Box) preStart() error {
monitor := taskmonitor.New(s.logger, C.StartTimeout)
monitor.Start("start logger")
@@ -576,28 +582,46 @@ func (s *Box) preStart() error {
if err != nil {
return E.Cause(err, "start logger")
}
s.scope.Add(s.logFactory.Close)
applyDebugOptions(s.debugOptions)
s.debugHTTPServer, err = startDebugHTTPServer(s.debugOptions)
debugHTTPServer, err := startDebugHTTPServer(s.debugOptions)
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if debugHTTPServer != nil {
s.scope.Add(debugHTTPServer.Close)
}
err = s.startInternalServices(adapter.StartStateInitialize) // cache-file clash-api v2ray-api
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.inbound, s.endpoint, s.service, s.certificateProvider)
err = s.startComponents(adapter.StartStateInitialize,
boxComponent{s.httpClientService.Name(), s.httpClientService},
boxComponent{"network", s.network},
boxComponent{"dns-transport", s.dnsTransport},
boxComponent{"dns-router", s.dnsRouter},
boxComponent{"connection", s.connection},
boxComponent{"router", s.router},
boxComponent{"outbound", s.outbound},
boxComponent{"provider", s.provider},
boxComponent{"endpoint", s.endpoint},
boxComponent{"certificate-provider", s.certificateProvider},
boxComponent{"inbound", s.inbound},
boxComponent{"service", s.service},
)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.outbound, s.provider, s.dnsTransport, s.network, s.connection)
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
err = s.startComponents(adapter.StartStateStart,
boxComponent{"outbound", s.outbound},
boxComponent{"provider", s.provider},
boxComponent{"dns-transport", s.dnsTransport},
boxComponent{"network", s.network},
boxComponent{"connection", s.connection},
boxComponent{s.httpClientService.Name(), s.httpClientService},
boxComponent{"router", s.router},
boxComponent{"dns-router", s.dnsRouter},
)
if err != nil {
return err
}
@@ -609,35 +633,65 @@ func (s *Box) start() error {
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStart, s.internalService)
err = s.startInternalServices(adapter.StartStateStart)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.endpoint)
if s.ntpService != nil {
done := adapter.LogElapsed(s.logger, "start ntp service")
err = s.ntpService.Start()
done()
if err != nil {
return E.Cause(err, "start ntp service")
}
s.scope.Add(s.ntpService.Close)
}
err = s.startComponents(adapter.StartStateStart,
boxComponent{"endpoint", s.endpoint},
boxComponent{"certificate-provider", s.certificateProvider},
boxComponent{"inbound", s.inbound},
boxComponent{"service", s.service},
)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.certificateProvider)
err = s.startComponents(adapter.StartStatePostStart,
boxComponent{"outbound", s.outbound},
boxComponent{"provider", s.provider},
boxComponent{"network", s.network},
boxComponent{"dns-transport", s.dnsTransport},
boxComponent{"dns-router", s.dnsRouter},
boxComponent{"connection", s.connection},
boxComponent{"router", s.router},
boxComponent{"endpoint", s.endpoint},
boxComponent{"certificate-provider", s.certificateProvider},
boxComponent{"inbound", s.inbound},
boxComponent{"service", s.service},
)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStart, s.inbound, s.service)
err = s.startInternalServices(adapter.StartStatePostStart)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStatePostStart, s.outbound, s.provider, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
err = s.startComponents(adapter.StartStateStarted,
boxComponent{"network", s.network},
boxComponent{"dns-transport", s.dnsTransport},
boxComponent{"dns-router", s.dnsRouter},
boxComponent{"connection", s.connection},
boxComponent{"router", s.router},
boxComponent{"outbound", s.outbound},
boxComponent{"provider", s.provider},
boxComponent{"endpoint", s.endpoint},
boxComponent{"certificate-provider", s.certificateProvider},
boxComponent{"inbound", s.inbound},
boxComponent{"service", s.service},
)
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStatePostStart, s.internalService)
if err != nil {
return err
}
err = adapter.Start(s.ctx, s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.provider, s.endpoint, s.certificateProvider, s.inbound, s.service)
if err != nil {
return err
}
err = adapter.StartNamed(s.ctx, s.logger, adapter.StartStateStarted, s.internalService)
err = s.startInternalServices(adapter.StartStateStarted)
if err != nil {
return err
}
@@ -645,62 +699,7 @@ func (s *Box) start() error {
}
func (s *Box) Close() error {
select {
case <-s.done:
return os.ErrClosed
default:
close(s.done)
}
var err error
if s.debugHTTPServer != nil {
err = E.Append(err, s.debugHTTPServer.Close(), func(err error) error {
return E.Cause(err, "close debug HTTP server")
})
s.debugHTTPServer = nil
}
for _, closeItem := range []struct {
name string
service adapter.Lifecycle
}{
{"service", s.service},
{"inbound", s.inbound},
{"certificate-provider", s.certificateProvider},
{"provider", s.provider},
{"endpoint", s.endpoint},
{"outbound", s.outbound},
{"router", s.router},
{"connection", s.connection},
{"dns-router", s.dnsRouter},
{"dns-transport", s.dnsTransport},
{"network", s.network},
} {
done := adapter.LogElapsed(s.logger, "close ", closeItem.name)
err = E.Append(err, closeItem.service.Close(), func(err error) error {
return E.Cause(err, "close ", closeItem.name)
})
done()
}
if s.httpClientService != nil {
s.logger.Trace("close ", s.httpClientService.Name())
startTime := time.Now()
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
return E.Cause(err, "close ", s.httpClientService.Name())
})
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
}
for _, lifecycleService := range s.internalService {
done := adapter.LogElapsed(s.logger, "close ", lifecycleService.Name())
err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name())
})
done()
}
done := adapter.LogElapsed(s.logger, "close logger")
err = E.Append(err, s.logFactory.Close(), func(err error) error {
return E.Cause(err, "close logger")
})
done()
return err
return s.scope.Close()
}
func (s *Box) Network() adapter.NetworkManager {
@@ -723,6 +722,14 @@ func (s *Box) Endpoint() adapter.EndpointManager {
return s.endpoint
}
func (s *Box) CreatedAt() time.Time {
return s.createdAt
}
func (s *Box) CloseIdleConnections() {
s.referenceManager.CloseIdleConnections()
}
func (s *Box) LogFactory() log.Factory {
return s.logFactory
}
Submodule clients/desktop updated: fb1175ca3b...50f5038491
+425 -235
View File
@@ -4,12 +4,14 @@ import (
"context"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"github.com/sagernet/asc-go/asc"
"github.com/sagernet/sing-box/cmd/internal/build_shared"
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
@@ -32,18 +34,37 @@ func main() {
if err != nil {
log.Fatal(err)
}
case "list_builds":
if len(os.Args) < 3 {
log.Fatal("platform required: ios, macos, or tvos")
}
err := listBuilds(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "remove_testflight":
if len(os.Args) < 5 {
log.Fatal("usage: remove_testflight <platform> <version> <build>")
}
err := removeTestflight(ctx, os.Args[2], os.Args[3], os.Args[4])
if err != nil {
log.Fatal(err)
}
case "cancel_app_store":
if len(os.Args) < 3 {
log.Fatal("platform required: ios, macos, or tvos")
}
err := cancelAppStore(ctx, os.Args[2])
if err != nil {
log.Fatal(err)
}
case "prepare_app_store":
err := prepareAppStore(ctx)
case "submit_app_store":
err := submitAppStore(ctx, os.Args[2:])
if err != nil {
log.Fatal(err)
}
case "publish_app_store":
err := publishAppStore(ctx)
err := publishAppStore(ctx, os.Args[2:])
if err != nil {
log.Fatal(err)
}
@@ -55,8 +76,35 @@ func main() {
const (
appID = "6785326793"
groupID = "39f9ebdc-05d4-421f-9595-dae71df227c4"
applePath = "../sing-box-for-apple"
appStorePromotionalText = "The universal proxy platform."
appStoreWhatsNew = "Fixes and improvements"
buildPollInterval = 15 * time.Second
)
var (
appStorePlatforms = []string{"ios", "tvos"}
platformDirectories = map[asc.Platform]string{
asc.PlatformIOS: "SFI",
asc.PlatformMACOS: "SFM",
asc.PlatformTVOS: "SFT",
}
)
func parsePlatform(name string) (asc.Platform, error) {
switch name {
case "ios":
return asc.PlatformIOS, nil
case "macos":
return asc.PlatformMACOS, nil
case "tvos":
return asc.PlatformTVOS, nil
default:
return "", E.New("unknown platform: ", name)
}
}
func createClient(expireDuration time.Duration) *asc.Client {
privateKey, err := os.ReadFile(os.Getenv("ASC_KEY_PATH"))
if err != nil {
@@ -70,16 +118,9 @@ func createClient(expireDuration time.Duration) *asc.Client {
}
func fetchNextProjectVersion(ctx context.Context, platformName string) error {
var platform asc.Platform
switch platformName {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", platformName)
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
query := &asc.ListBuildsQuery{
@@ -115,20 +156,55 @@ func fetchNextProjectVersion(ctx context.Context, platformName string) error {
return nil
}
func listBuilds(ctx context.Context, platformName string) error {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
client := createClient(time.Minute)
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
if err != nil {
return err
}
betaBuildIDs := common.Map(buildIDsResponse.Data, func(it asc.RelationshipData) string {
return it.ID
})
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
Include: []string{"preReleaseVersion"},
Sort: []string{"-uploadedDate"},
Limit: 10,
})
if err != nil {
return err
}
for _, build := range builds.Data {
var betaGroup string
if slices.Contains(betaBuildIDs, build.ID) {
betaGroup = " testflight"
}
var versionString string
if build.Relationships != nil && build.Relationships.PreReleaseVersion != nil && build.Relationships.PreReleaseVersion.Data != nil {
for _, included := range builds.Included {
preReleaseVersion := included.PrereleaseVersion()
if preReleaseVersion != nil && preReleaseVersion.ID == build.Relationships.PreReleaseVersion.Data.ID {
versionString = *preReleaseVersion.Attributes.Version
}
}
}
os.Stdout.WriteString(F.ToString(versionString, " (", *build.Attributes.Version, ") ", *build.Attributes.ProcessingState, " ", build.Attributes.UploadedDate.Time.Format(time.RFC3339), betaGroup, "\n"))
}
return nil
}
func publishTestflight(ctx context.Context) error {
if len(os.Args) < 3 {
return E.New("platform required: ios, macos, or tvos")
}
var platform asc.Platform
switch os.Args[2] {
case "ios":
platform = asc.PlatformIOS
case "macos":
platform = asc.PlatformMACOS
case "tvos":
platform = asc.PlatformTVOS
default:
return E.New("unknown platform: ", os.Args[2])
platform, err := parsePlatform(os.Args[2])
if err != nil {
return err
}
tagVersion, err := build_shared.ReadTagVersion()
@@ -136,6 +212,8 @@ func publishTestflight(ctx context.Context) error {
return err
}
tag := tagVersion.VersionString()
testFlightVersion := build_shared.TestFlightVersion(tagVersion)
projectVersion := os.Getenv(strings.ToUpper(os.Args[2]) + "_PROJECT_VERSION")
releaseNotes := F.ToString("sing-box ", tagVersion.String())
if len(os.Args) >= 4 {
@@ -153,27 +231,37 @@ func publishTestflight(ctx context.Context) error {
return it.ID
})
waitingForProcess := false
log.Info(string(platform), " list builds")
query := &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
FilterPreReleaseVersionVersion: []string{testFlightVersion},
Sort: []string{"-uploadedDate"},
Limit: 1,
}
if projectVersion != "" {
query.FilterVersion = []string{projectVersion}
}
log.Info(string(platform), " ", testFlightVersion, " (", projectVersion, ") list builds")
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
builds, _, err := client.Builds.ListBuilds(ctx, query)
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Info(string(platform), " ", testFlightVersion, " waiting for build upload")
time.Sleep(buildPollInterval)
continue
}
build := builds.Data[0]
log.Info(string(platform), " ", tag, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
log.Info(string(platform), " ", testFlightVersion, " found build: ", build.ID, " (", *build.Attributes.Version, ")")
if projectVersion == "" && common.Contains(buildIDs, build.ID) {
log.Info(string(platform), " ", testFlightVersion, " build ", *build.Attributes.Version, " already published, waiting for new upload")
time.Sleep(buildPollInterval)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
log.Info(string(platform), " ", testFlightVersion, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(buildPollInterval)
continue
}
log.Info(string(platform), " ", tag, " list localizations")
@@ -198,7 +286,7 @@ func publishTestflight(ctx context.Context) error {
response, err := client.TestFlight.AddBuildsToBetaGroup(ctx, groupID, []string{build.ID})
if response != nil && (response.StatusCode == http.StatusUnprocessableEntity || response.StatusCode == http.StatusNotFound) {
log.Info("waiting for process")
time.Sleep(15 * time.Second)
time.Sleep(buildPollInterval)
continue
} else if err != nil {
return err
@@ -226,227 +314,341 @@ func publishTestflight(ctx context.Context) error {
return nil
}
func cancelAppStore(ctx context.Context, platform string) error {
switch platform {
case "ios":
platform = string(asc.PlatformIOS)
case "macos":
platform = string(asc.PlatformMACOS)
case "tvos":
platform = string(asc.PlatformTVOS)
}
tag, err := build_shared.ReadTag()
func removeTestflight(ctx context.Context, platformName string, versionString string, buildNumber string) error {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
client := createClient(time.Minute)
for {
log.Info(platform, " list versions")
versions, response, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if isRetryable(response) {
continue
} else if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
if version.ID == "" {
return nil
}
log.Info(platform, " ", tag, " get submission")
submission, response, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if response != nil && response.StatusCode == http.StatusNotFound {
return nil
}
if isRetryable(response) {
continue
} else if err != nil {
return err
}
log.Info(platform, " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
return nil
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
FilterPreReleaseVersionVersion: []string{versionString},
FilterVersion: []string{buildNumber},
})
if err != nil {
return err
}
if len(builds.Data) != 1 {
return E.New(string(platform), " ", versionString, " (", buildNumber, ") matched ", len(builds.Data), " builds")
}
build := builds.Data[0]
log.Info(string(platform), " ", versionString, " (", buildNumber, ") remove from beta group")
_, err = client.Builds.RemoveAccessForBetaGroupsFromBuild(ctx, build.ID, []string{groupID})
if err != nil {
return err
}
return nil
}
func prepareAppStore(ctx context.Context) error {
func cancelAppStore(ctx context.Context, platformName string) error {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
tag, err := build_shared.ReadTag()
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
FilterVersionString: []string{tag},
})
if err != nil {
return err
}
if len(versions.Data) == 0 {
return nil
}
versionID := versions.Data[0].ID
log.Info(string(platform), " ", tag, " list review submissions")
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
FilterApp: []string{appID},
FilterPlatform: []string{string(platform)},
FilterState: []string{
string(asc.ReviewSubmissionStateReadyForReview),
string(asc.ReviewSubmissionStateWaitingForReview),
string(asc.ReviewSubmissionStateInReview),
},
})
if err != nil {
return err
}
for _, submission := range submissions.Data {
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, submission.ID, versionID)
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
})
log.Info(string(platform), " ", tag, " list builds")
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
})
if !containsVersion {
continue
}
log.Info(string(platform), " ", tag, " cancel review submission ", submission.ID)
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, nil, common.Ptr(true))
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Fatal(string(platform), " ", tag, " no build found")
}
buildID := common.Ptr(builds.Data[0].ID)
if version.ID == "" {
log.Info(string(platform), " ", tag, " create version")
newVersion, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
VersionString: tag,
}, appID, buildID)
if err != nil {
return err
}
version = newVersion.Data
} else {
log.Info(string(platform), " ", tag, " check build")
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != *buildID {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
submission, _, err := client.Submission.GetAppStoreVersionSubmissionForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
if submission != nil {
log.Info(string(platform), " ", tag, " delete submission")
_, err = client.Submission.DeleteSubmission(ctx, submission.Data.ID)
if err != nil {
return err
}
time.Sleep(5 * time.Second)
}
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
log.Info(string(platform), " ", tag, " update build")
response, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, buildID)
if err != nil {
return err
}
if response.StatusCode != http.StatusNoContent {
response.Write(os.Stderr)
log.Fatal(string(platform), " ", tag, " unexpected response: ", response.Status)
}
} else {
switch *version.Attributes.AppStoreState {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected:
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview,
asc.AppStoreVersionStatePendingDeveloperRelease:
continue
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
}
}
}
log.Info(string(platform), " ", tag, " list localization")
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Info(string(platform), " ", tag, " no en-US localization found")
}
if localization.Attributes == nil || localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew == "" {
log.Info(string(platform), " ", tag, " update localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr("Yet another distribution for sing-box, the universal proxy platform."),
WhatsNew: common.Ptr(F.ToString("sing-box ", tag, ": Fixes and improvements.")),
})
if err != nil {
return err
}
}
log.Info(string(platform), " ", tag, " create submission")
fixSubmit:
for {
_, response, err := client.Submission.CreateSubmission(ctx, version.ID)
if err != nil {
switch response.StatusCode {
case http.StatusInternalServerError:
continue
default:
return err
}
}
switch response.StatusCode {
case http.StatusCreated:
break fixSubmit
default:
return err
}
}
}
return nil
}
func publishAppStore(ctx context.Context) error {
tag, err := build_shared.ReadTag()
func reviewSubmissionContainsVersion(ctx context.Context, client *asc.Client, submissionID string, versionID string) (bool, error) {
items, _, err := client.Submission.ListItemsForReviewSubmission(ctx, submissionID, nil)
if err != nil {
return false, err
}
return slices.ContainsFunc(items.Data, func(it asc.ReviewSubmissionItem) bool {
return it.Relationships != nil && it.Relationships.AppStoreVersion != nil &&
it.Relationships.AppStoreVersion.Data != nil && it.Relationships.AppStoreVersion.Data.ID == versionID
}), nil
}
func submitAppStore(ctx context.Context, platformNames []string) error {
if len(platformNames) == 0 {
platformNames = appStorePlatforms
}
client := createClient(20 * time.Minute)
for _, platformName := range platformNames {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
if err != nil {
return err
}
err = submitAppStoreVersion(ctx, client, platform, versionString)
if err != nil {
return err
}
}
return nil
}
func submitAppStoreVersion(ctx context.Context, client *asc.Client, platform asc.Platform, versionString string) error {
log.Info(string(platform), " ", versionString, " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
})
if err != nil {
return err
}
client := createClient(time.Minute)
for _, platform := range []asc.Platform{
asc.PlatformIOS,
asc.PlatformMACOS,
asc.PlatformTVOS,
} {
log.Info(string(platform), " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
localVersion := badversion.Parse(versionString)
var version asc.AppStoreVersion
for _, remoteVersion := range versions.Data {
remoteVersionString := *remoteVersion.Attributes.VersionString
if remoteVersionString == versionString {
version = remoteVersion
continue
}
if !badversion.Parse(remoteVersionString).LessThan(localVersion) {
return E.New(string(platform), " ", versionString, " is not newer than App Store version ", remoteVersionString)
}
}
if version.ID != "" {
state := *version.Attributes.AppStoreState
switch state {
case asc.AppStoreVersionStatePrepareForSubmission,
asc.AppStoreVersionStateRejected,
asc.AppStoreVersionStateDeveloperRejected,
asc.AppStoreVersionStateMetadataRejected,
asc.AppStoreVersionStateInvalidBinary:
log.Info(string(platform), " ", versionString, " found version in state ", string(state))
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
return nil
case asc.AppStoreVersionStatePendingDeveloperRelease:
log.Info(string(platform), " ", versionString, " approved, run publish_app_store to release")
return nil
default:
return E.New(string(platform), " ", versionString, " already released: ", string(state))
}
} else {
log.Info(string(platform), " ", versionString, " create version")
created, _, err := client.Apps.CreateAppStoreVersion(ctx, asc.AppStoreVersionCreateRequestAttributes{
Platform: platform,
ReleaseType: common.Ptr("MANUAL"),
VersionString: versionString,
}, appID, nil)
if err != nil {
return err
}
version = created.Data
}
log.Info(string(platform), " ", versionString, " list localizations")
localizations, _, err := client.Apps.ListLocalizationsForAppStoreVersion(ctx, version.ID, nil)
if err != nil {
return err
}
localization := common.Find(localizations.Data, func(it asc.AppStoreVersionLocalization) bool {
return *it.Attributes.Locale == "en-US"
})
if localization.ID == "" {
log.Info(string(platform), " ", versionString, " create en-US localization")
_, _, err = client.Apps.CreateAppStoreVersionLocalization(ctx, asc.AppStoreVersionLocalizationCreateRequestAttributes{
Locale: "en-US",
PromotionalText: common.Ptr(appStorePromotionalText),
WhatsNew: common.Ptr(appStoreWhatsNew),
}, version.ID)
if err != nil {
return err
}
} else if localization.Attributes.PromotionalText == nil || *localization.Attributes.PromotionalText != appStorePromotionalText ||
localization.Attributes.WhatsNew == nil || *localization.Attributes.WhatsNew != appStoreWhatsNew {
log.Info(string(platform), " ", versionString, " update en-US localization")
_, _, err = client.Apps.UpdateAppStoreVersionLocalization(ctx, localization.ID, &asc.AppStoreVersionLocalizationUpdateRequestAttributes{
PromotionalText: common.Ptr(appStorePromotionalText),
WhatsNew: common.Ptr(appStoreWhatsNew),
})
if err != nil {
return err
}
version := common.Find(versions.Data, func(it asc.AppStoreVersion) bool {
return *it.Attributes.VersionString == tag
}
var build asc.Build
for {
builds, _, err := client.Builds.ListBuilds(ctx, &asc.ListBuildsQuery{
FilterApp: []string{appID},
FilterPreReleaseVersionPlatform: []string{string(platform)},
FilterPreReleaseVersionVersion: []string{versionString},
Sort: []string{"-uploadedDate"},
Limit: 1,
})
switch *version.Attributes.AppStoreState {
if err != nil {
return err
}
if len(builds.Data) == 0 {
log.Info(string(platform), " ", versionString, " waiting for build upload")
time.Sleep(buildPollInterval)
continue
}
build = builds.Data[0]
processingState := *build.Attributes.ProcessingState
if processingState == "VALID" {
break
}
if processingState == "PROCESSING" {
log.Info(string(platform), " ", versionString, " waiting for build ", *build.Attributes.Version, ": ", processingState)
time.Sleep(buildPollInterval)
continue
}
return E.New(string(platform), " ", versionString, " build ", *build.Attributes.Version, " ", processingState)
}
log.Info(string(platform), " ", versionString, " found build ", *build.Attributes.Version)
currentBuild, response, err := client.Apps.GetBuildIDForAppStoreVersion(ctx, version.ID)
if err != nil {
return err
}
if response.StatusCode != http.StatusOK || currentBuild.Data.ID != build.ID {
log.Info(string(platform), " ", versionString, " attach build ", *build.Attributes.Version)
_, err = client.Apps.UpdateBuildForAppStoreVersion(ctx, version.ID, common.Ptr(build.ID))
if err != nil {
return err
}
}
log.Info(string(platform), " ", versionString, " list review submissions")
submissions, _, err := client.Submission.ListReviewSubmissions(ctx, &asc.ListReviewSubmissionsQuery{
FilterApp: []string{appID},
FilterPlatform: []string{string(platform)},
FilterState: []string{
string(asc.ReviewSubmissionStateReadyForReview),
string(asc.ReviewSubmissionStateWaitingForReview),
string(asc.ReviewSubmissionStateInReview),
string(asc.ReviewSubmissionStateUnresolvedIssues),
},
})
if err != nil {
return err
}
var (
submission asc.ReviewSubmission
submissionHasItem bool
)
for _, remoteSubmission := range submissions.Data {
containsVersion, err := reviewSubmissionContainsVersion(ctx, client, remoteSubmission.ID, version.ID)
if err != nil {
return err
}
state := *remoteSubmission.Attributes.State
switch state {
case asc.ReviewSubmissionStateReadyForReview:
submission = remoteSubmission
submissionHasItem = containsVersion
case asc.ReviewSubmissionStateWaitingForReview, asc.ReviewSubmissionStateInReview:
if containsVersion {
log.Info(string(platform), " ", versionString, " already submitted: ", string(state))
return nil
}
return E.New(string(platform), " review submission ", remoteSubmission.ID, " with another version is ", string(state))
default:
return E.New(string(platform), " review submission ", remoteSubmission.ID, " is ", string(state))
}
}
if submission.ID == "" {
log.Info(string(platform), " ", versionString, " create review submission")
created, _, err := client.Submission.CreateReviewSubmission(ctx, appID, common.Ptr(platform))
if err != nil {
return err
}
submission = created.Data
}
if !submissionHasItem {
log.Info(string(platform), " ", versionString, " add version to review submission ", submission.ID)
_, _, err = client.Submission.CreateReviewSubmissionItemForAppStoreVersion(ctx, submission.ID, version.ID)
if err != nil {
return err
}
}
log.Info(string(platform), " ", versionString, " submit review submission ", submission.ID)
_, _, err = client.Submission.UpdateReviewSubmission(ctx, submission.ID, common.Ptr(true), nil)
if err != nil {
return err
}
return nil
}
func publishAppStore(ctx context.Context, platformNames []string) error {
if len(platformNames) == 0 {
platformNames = appStorePlatforms
}
client := createClient(time.Minute)
for _, platformName := range platformNames {
platform, err := parsePlatform(platformName)
if err != nil {
return err
}
versionString, err := build_shared.ReadAppleMarketingVersion(applePath, platformDirectories[platform])
if err != nil {
return err
}
log.Info(string(platform), " ", versionString, " list versions")
versions, _, err := client.Apps.ListAppStoreVersionsForApp(ctx, appID, &asc.ListAppStoreVersionsQuery{
FilterPlatform: []string{string(platform)},
FilterVersionString: []string{versionString},
})
if err != nil {
return err
}
if len(versions.Data) == 0 {
return E.New(string(platform), " ", versionString, " not found")
}
version := versions.Data[0]
state := *version.Attributes.AppStoreState
switch state {
case asc.AppStoreVersionStatePrepareForSubmission, asc.AppStoreVersionStateDeveloperRejected:
log.Fatal(string(platform), " ", tag, " not submitted")
case asc.AppStoreVersionStateWaitingForReview,
asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", tag, " waiting for review")
return E.New(string(platform), " ", versionString, " not submitted")
case asc.AppStoreVersionStateWaitingForReview, asc.AppStoreVersionStateInReview:
log.Warn(string(platform), " ", versionString, " waiting for review")
continue
case asc.AppStoreVersionStatePendingDeveloperRelease:
default:
log.Fatal(string(platform), " ", tag, " unknown state ", string(*version.Attributes.AppStoreState))
return E.New(string(platform), " ", versionString, " unknown state ", string(state))
}
log.Info(string(platform), " ", versionString, " release")
_, _, err = client.Publishing.CreatePhasedRelease(ctx, common.Ptr(asc.PhasedReleaseStateComplete), version.ID)
if err != nil {
return err
@@ -454,15 +656,3 @@ func publishAppStore(ctx context.Context) error {
}
return nil
}
func isRetryable(response *asc.Response) bool {
if response == nil {
return false
}
switch response.StatusCode {
case http.StatusInternalServerError, http.StatusUnprocessableEntity:
return true
default:
return false
}
}
+47
View File
@@ -0,0 +1,47 @@
package build_shared
import (
"os"
"path/filepath"
"strings"
E "github.com/sagernet/sing/common/exceptions"
"howett.net/plist"
)
func ReadAppleMarketingVersion(applePath string, directory string) (string, error) {
projectFile, err := os.Open(filepath.Join(applePath, "sing-box.xcodeproj", "project.pbxproj"))
if err != nil {
return "", err
}
defer projectFile.Close()
var project map[string]any
err = plist.NewDecoder(projectFile).Decode(&project)
if err != nil {
return "", err
}
var marketingVersion string
for _, object := range project["objects"].(map[string]any) {
buildSettings, isMap := object.(map[string]any)["buildSettings"].(map[string]any)
if !isMap {
continue
}
infoPlistFile, isString := buildSettings["INFOPLIST_FILE"].(string)
if !isString || !strings.HasPrefix(infoPlistFile, directory+"/") {
continue
}
version, isString := buildSettings["MARKETING_VERSION"].(string)
if !isString {
continue
}
if marketingVersion != "" && marketingVersion != version {
return "", E.New("inconsistent marketing versions in ", directory, ": ", marketingVersion, ", ", version)
}
marketingVersion = version
}
if marketingVersion == "" {
return "", E.New("marketing version not found in ", directory)
}
return marketingVersion, nil
}
+4
View File
@@ -80,3 +80,7 @@ func (c *ReadWaitConn) Upstream() any {
func (c *ReadWaitConn) ReaderReplaceable() bool {
return true
}
func (c *ReadWaitConn) WriterReplaceable() bool {
return true
}
+42 -33
View File
@@ -31,6 +31,7 @@ type Store struct {
certificatePaths []string
certificateDirectoryPaths []string
watcher *fswatch.Watcher
anchorPEM []byte
//nolint:unused // populated only on darwin && cgo via the storePlatform embed.
platform storePlatform
}
@@ -92,10 +93,12 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
}
store.watcher = watcher
}
err := store.update()
currentPool, anchorPEM, err := store.load()
if err != nil {
return nil, E.Cause(err, "initializing certificate store")
}
store.currentPool = currentPool
store.anchorPEM = anchorPEM
return store, nil
}
@@ -103,31 +106,28 @@ func (s *Store) Name() string {
return "certificate"
}
func (s *Store) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.watcher != nil {
return s.watcher.Start()
func (s *Store) Start(stage adapter.StartStage, scope *adapter.Scope) error {
switch stage {
case adapter.StartStateInitialize:
s.access.Lock()
err := s.updatePlatformLocked(s.anchorPEM)
s.access.Unlock()
if err != nil {
return err
}
scope.Add(s.closePlatform)
case adapter.StartStateStart:
if s.watcher != nil {
err := s.watcher.Start()
if err != nil {
return err
}
scope.Add(s.watcher.Close)
}
}
return nil
}
func (s *Store) Close() error {
watcher := s.watcher
s.watcher = nil
var closeErr error
if watcher != nil {
closeErr = watcher.Close()
}
platformErr := s.closePlatform()
if platformErr != nil {
closeErr = platformErr
}
return closeErr
}
func (s *Store) Pool() *x509.CertPool {
s.access.RLock()
defer s.access.RUnlock()
@@ -143,38 +143,50 @@ func (s *Store) ExclusiveAnchors() bool {
}
func (s *Store) update() error {
currentPool, err := s.newBasePool()
currentPool, anchorPEM, err := s.load()
if err != nil {
return err
}
s.access.Lock()
defer s.access.Unlock()
s.currentPool = currentPool
s.anchorPEM = anchorPEM
return s.updatePlatformLocked(anchorPEM)
}
func (s *Store) load() (*x509.CertPool, []byte, error) {
currentPool, err := s.newBasePool()
if err != nil {
return nil, nil, err
}
pemBuffer := new(bytes.Buffer)
switch s.storeType {
case C.CertificateStoreMozilla:
pemContent := mozillaIncludedPEM()
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
return E.New("invalid Mozilla included certificate PEM")
return nil, nil, E.New("invalid Mozilla included certificate PEM")
}
appendPEMBlock(pemBuffer, string(pemContent))
case C.CertificateStoreChrome:
pemContent := chromeIncludedPEM()
if !currentPool.AppendCertsFromPEM([]byte(pemContent)) {
return E.New("invalid Chrome included certificate PEM")
return nil, nil, E.New("invalid Chrome included certificate PEM")
}
appendPEMBlock(pemBuffer, string(pemContent))
}
if s.certificate != "" {
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
return E.New("invalid certificate PEM strings")
return nil, nil, E.New("invalid certificate PEM strings")
}
appendPEMBlock(pemBuffer, s.certificate)
}
for _, path := range s.certificatePaths {
pemContent, err := filemanager.ReadFile(s.ctx, path)
if err != nil {
return err
return nil, nil, err
}
if !currentPool.AppendCertsFromPEM(pemContent) {
return E.New("invalid certificate PEM file: ", path)
return nil, nil, E.New("invalid certificate PEM file: ", path)
}
appendPEMBlock(pemBuffer, string(pemContent))
}
@@ -195,12 +207,9 @@ func (s *Store) update() error {
}
}
if firstErr != nil {
return firstErr
return nil, nil, firstErr
}
s.access.Lock()
defer s.access.Unlock()
s.currentPool = currentPool
return s.updatePlatformLocked(pemBuffer.Bytes())
return currentPool, pemBuffer.Bytes(), nil
}
func appendPEMBlock(buffer *bytes.Buffer, block string) {
+29 -1
View File
@@ -199,7 +199,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else {
udpFragment = options.UDPFragmentDefault
}
if !udpFragment {
if udpFragment {
dialer.Control = control.Append(dialer.Control, control.EnableUDPFragment())
listener.Control = control.Append(listener.Control, control.EnableUDPFragment())
} else {
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
}
@@ -431,6 +434,15 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr,
if err != nil {
return conn, err
}
if nativeConn, isUDPConn := conn.(*net.UDPConn); isUDPConn {
var rawConn syscall.RawConn
rawConn, err = nativeConn.SyscallConn()
if err != nil {
conn.Close()
return nil, err
}
conn = &udpConn{Conn: conn, rawConn: rawConn}
}
if d.connectionManager != nil {
conn = d.connectionManager.TrackConn(conn)
}
@@ -439,9 +451,17 @@ func (d *DefaultDialer) trackConn(ctx context.Context, destination M.Socksaddr,
if recorder != nil {
recorder.CountConnectionOpened()
attribution := d.dialAttribution(ctx, destination)
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
outboundCounter.CountDial()
dnsCounter.CountDial()
conn = bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
outboundCounter.CountIn(n)
dnsCounter.CountIn(n)
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
outboundCounter.CountOut(n)
dnsCounter.CountOut(n)
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}})
}
@@ -461,9 +481,17 @@ func (d *DefaultDialer) trackPacketConn(ctx context.Context, destination M.Socks
if recorder != nil {
recorder.CountConnectionOpened()
attribution := d.dialAttribution(ctx, destination)
outboundCounter := recorder.TrafficCounter(powerreport.TrafficOutbound, attribution.Outbound)
dnsCounter := recorder.TrafficCounter(powerreport.TrafficDNSTransport, attribution.DNS)
outboundCounter.CountDial()
dnsCounter.CountDial()
conn = bufio.NewNetPacketConn(bufio.NewCounterPacketConn(bufio.NewPacketConn(conn), []N.CountFunc{func(n int64) {
outboundCounter.CountIn(n)
dnsCounter.CountIn(n)
recorder.Touch(powerreport.DirectionInbound, int(n), attribution)
}}, []N.CountFunc{func(n int64) {
outboundCounter.CountOut(n)
dnsCounter.CountOut(n)
recorder.Touch(powerreport.DirectionOutbound, int(n), attribution)
}}))
}
+62 -64
View File
@@ -63,75 +63,31 @@ func NewWithOptions(options Options) (N.Dialer, error) {
}
}
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
var defaultOptions adapter.NetworkOptions
if networkManager != nil {
defaultOptions = networkManager.DefaultOptions()
}
var (
server string
dnsQueryOptions adapter.DNSQueryOptions
resolveFallbackDelay time.Duration
server string
dnsQueryOptions adapter.DNSQueryOptions
)
if dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "" {
var transport adapter.DNSTransport
if !options.DirectResolver {
var loaded bool
transport, loaded = dnsTransport.Transport(dialOptions.DomainResolver.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + dialOptions.DomainResolver.Server)
}
hasDomainResolver := dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != ""
if options.DirectResolver {
if !hasDomainResolver {
return nil, E.New("missing domain resolver for domain server address")
}
var strategy C.DomainStrategy
if dialOptions.DomainResolver.Strategy != option.DomainStrategy(C.DomainStrategyAsIS) {
strategy = C.DomainStrategy(dialOptions.DomainResolver.Strategy)
} else if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
server = dialOptions.DomainResolver.Server
dnsQueryOptions = adapter.DNSQueryOptions{
Transport: transport,
Strategy: strategy,
Timeout: time.Duration(dialOptions.DomainResolver.Timeout),
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 {
return nil, E.New("missing domain resolver for domain server address")
dnsQueryOptions = domainResolveQueryOptions(dialOptions.DomainResolver)
} else {
if defaultOptions.DomainResolver != "" {
dnsQueryOptions = defaultOptions.DomainResolveOptions
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return nil, E.New("default domain resolver not found: " + defaultOptions.DomainResolver)
}
dnsQueryOptions.Transport = transport
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
} else {
transports := dnsTransport.Transports()
if len(transports) < 2 {
dnsQueryOptions.Transport = dnsTransport.Default()
} else if options.NewDialer {
return nil, E.New("missing domain resolver for domain server address")
} else {
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
}
dnsQueryOptions, err = NewDNSQueryOptions(options.Context, dialOptions.DomainResolver, options.NewDialer)
if err != nil {
return nil, err
}
if
}
if hasDomainResolver {
server = dialOptions.DomainResolver.Server
}
if
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) && (!hasDomainResolver || dialOptions.DomainResolver.Strategy == option.DomainStrategy(C.DomainStrategyAsIS)) {
//nolint:staticcheck
dialOptions.DomainStrategy != option.DomainStrategy(C.DomainStrategyAsIS) {
//nolint:staticcheck
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
dnsQueryOptions.Strategy = C.DomainStrategy(dialOptions.DomainStrategy)
deprecated.Report(options.Context, deprecated.OptionLegacyDomainStrategyOptions)
}
dialer = NewResolveDialer(
options.Context,
@@ -139,12 +95,54 @@ func NewWithOptions(options Options) (N.Dialer, error) {
dialOptions.Detour == "" && !dialOptions.TCPFastOpen,
server,
dnsQueryOptions,
resolveFallbackDelay,
time.Duration(dialOptions.FallbackDelay),
)
}
return dialer, nil
}
func NewDNSQueryOptions(ctx context.Context, domainResolver *option.DomainResolveOptions, newDialer bool) (adapter.DNSQueryOptions, error) {
dnsTransport := service.FromContext[adapter.DNSTransportManager](ctx)
if domainResolver != nil && domainResolver.Server != "" {
transport, loaded := dnsTransport.Transport(domainResolver.Server)
if !loaded {
return adapter.DNSQueryOptions{}, E.New("domain resolver not found: ", domainResolver.Server)
}
dnsQueryOptions := domainResolveQueryOptions(domainResolver)
dnsQueryOptions.Transport = transport
return dnsQueryOptions, nil
}
defaultOptions := service.FromContext[adapter.NetworkManager](ctx).DefaultOptions()
if defaultOptions.DomainResolver != "" {
transport, loaded := dnsTransport.Transport(defaultOptions.DomainResolver)
if !loaded {
return adapter.DNSQueryOptions{}, E.New("default domain resolver not found: ", defaultOptions.DomainResolver)
}
dnsQueryOptions := defaultOptions.DomainResolveOptions
dnsQueryOptions.Transport = transport
return dnsQueryOptions, nil
}
if len(dnsTransport.Transports()) < 2 {
return adapter.DNSQueryOptions{Transport: dnsTransport.Default()}, nil
}
if newDialer {
return adapter.DNSQueryOptions{}, E.New("missing domain resolver for domain server address")
}
deprecated.Report(ctx, deprecated.OptionMissingDomainResolver)
return adapter.DNSQueryOptions{}, nil
}
func domainResolveQueryOptions(domainResolver *option.DomainResolveOptions) adapter.DNSQueryOptions {
return adapter.DNSQueryOptions{
Strategy: C.DomainStrategy(domainResolver.Strategy),
Timeout: time.Duration(domainResolver.Timeout),
DisableCache: domainResolver.DisableCache,
DisableOptimisticCache: domainResolver.DisableOptimisticCache,
RewriteTTL: domainResolver.RewriteTTL,
ClientSubnet: domainResolver.ClientSubnet.Build(netip.Prefix{}),
}
}
type ParallelInterfaceDialer interface {
N.Dialer
DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
+13 -2
View File
@@ -20,6 +20,7 @@ import (
type slowOpenConn struct {
dialer *tfo.Dialer
ctx context.Context
cancel context.CancelFunc
network string
destination M.Socksaddr
conn atomic.Pointer[net.TCPConn]
@@ -39,9 +40,11 @@ func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, de
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
}
}
dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
return &slowOpenConn{
dialer: dialer,
ctx: ctx,
ctx: dialCtx,
cancel: cancel,
network: network,
destination: destination,
create: make(chan struct{}),
@@ -86,7 +89,14 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if err != nil {
c.err = err
} else {
c.conn.Store(conn.(*net.TCPConn))
select {
case <-c.done:
conn.Close()
err = os.ErrClosed
c.err = err
default:
c.conn.Store(conn.(*net.TCPConn))
}
}
n = len(b)
close(c.create)
@@ -96,6 +106,7 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
func (c *slowOpenConn) Close() error {
c.closeOnce.Do(func() {
close(c.done)
c.cancel()
conn := c.conn.Load()
if conn != nil {
conn.Close()
+53
View File
@@ -0,0 +1,53 @@
package dialer
import (
"io"
"net"
"syscall"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
)
type udpConn struct {
net.Conn
rawConn syscall.RawConn
}
func (c *udpConn) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
if n == 0 && err == nil {
var isEOF bool
c.rawConn.Control(func(fd uintptr) {
isEOF = control.IsUDPEOF(fd)
})
if isEOF {
err = io.EOF
}
}
return
}
func (c *udpConn) SyscallConn() (syscall.RawConn, error) {
return c.rawConn, nil
}
func (c *udpConn) Upstream() any {
return c.Conn
}
func (c *udpConn) ReaderReplaceable() bool {
return true
}
func (c *udpConn) WriterReplaceable() bool {
return true
}
var (
_ syscall.Conn = (*udpConn)(nil)
_ common.WithUpstream = (*udpConn)(nil)
_ N.ReaderWithUpstream = (*udpConn)(nil)
_ N.WriterWithUpstream = (*udpConn)(nil)
)
+13 -9
View File
@@ -40,10 +40,12 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
return nil, err
}
return &ManagedTransport{
dialer: rawDialer,
headers: headers,
host: host,
tag: tag,
dialer: rawDialer,
headers: headers,
host: host,
tag: tag,
detour: options.Detour,
defaultOutbound: options.DefaultOutbound,
factory: func() (innerTransport, error) {
return newAppleTransport(ctx, logger, rawDialer, options)
},
@@ -69,11 +71,13 @@ func NewTransport(ctx context.Context, logger logger.ContextLogger, tag string,
return nil, err
}
managedTransport := &ManagedTransport{
cheapRebuild: cheapRebuild,
dialer: rawDialer,
headers: headers,
host: host,
tag: tag,
cheapRebuild: cheapRebuild,
dialer: rawDialer,
headers: headers,
host: host,
tag: tag,
detour: options.Detour,
defaultOutbound: options.DefaultOutbound,
factory: func() (innerTransport, error) {
return newTransport(rawDialer, baseTLSConfig, options)
},
+52 -31
View File
@@ -6,6 +6,7 @@ import (
"context"
stdTLS "crypto/tls"
"errors"
"io"
"net/http"
"sync"
"time"
@@ -172,41 +173,49 @@ func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Re
}
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, authority string) (*http.Response, error) {
ctx, cancel := context.WithCancel(request.Context())
defer cancel()
type result struct {
response *http.Response
err error
h3 bool
}
results := make(chan result, 2)
startRoundTrip := func(request *http.Request, useH3 bool) {
request = request.WithContext(ctx)
var (
response *http.Response
err error
)
if useH3 {
response, err = t.h3Transport.RoundTrip(request)
} else {
response, err = t.h2FallbackRoundTrip(request)
}
results <- result{response: response, err: err, h3: useH3}
var (
results = make(chan result, 2)
cancels []context.CancelFunc
received int
)
startRoundTrip := func(useH3 bool) {
ctx, cancel := context.WithCancel(request.Context())
cancels = append(cancels, cancel)
raceRequest := cloneRequestForRetry(request).WithContext(ctx)
go func() {
var (
response *http.Response
err error
)
if useH3 {
response, err = t.h3Transport.RoundTrip(raceRequest)
} else {
response, err = t.h2FallbackRoundTrip(raceRequest)
}
results <- result{response: response, err: err, h3: useH3}
}()
}
goroutines := 1
received := 0
drainRemaining := func() {
cancel()
for range goroutines - received {
finish := func(winner int) {
for index, cancel := range cancels {
if index != winner {
cancel()
}
}
for range len(cancels) - received {
go func() {
loser := <-results
if loser.response != nil && loser.response.Body != nil {
if loser.response != nil {
loser.response.Body.Close()
}
}()
}
}
go startRoundTrip(cloneRequestForRetry(request), true)
startRoundTrip(true)
timer := time.NewTimer(t.fallbackDelay)
defer timer.Stop()
var (
@@ -216,39 +225,40 @@ func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, autho
for {
select {
case <-timer.C:
if goroutines == 1 {
goroutines++
go startRoundTrip(cloneRequestForRetry(request), false)
if len(cancels) == 1 {
startRoundTrip(false)
}
case raceResult := <-results:
received++
if raceResult.err == nil {
winner := 1
if raceResult.h3 {
winner = 0
t.clearH3Broken(authority)
}
drainRemaining()
finish(winner)
raceResult.response.Body = &cancelOnCloseBody{ReadCloser: raceResult.response.Body, cancel: cancels[winner]}
return raceResult.response, nil
}
if raceResult.h3 {
t.markH3Broken(authority)
h3Err = raceResult.err
if goroutines == 1 {
goroutines++
if len(cancels) == 1 {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
go startRoundTrip(cloneRequestForRetry(request), false)
startRoundTrip(false)
}
} else {
fallbackErr = raceResult.err
}
if received < goroutines {
if received < len(cancels) {
continue
}
drainRemaining()
finish(-1)
switch {
case h3Err != nil && fallbackErr != nil:
return nil, E.Errors(h3Err, fallbackErr)
@@ -261,6 +271,17 @@ func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request, autho
}
}
type cancelOnCloseBody struct {
io.ReadCloser
cancel context.CancelFunc
}
func (b *cancelOnCloseBody) Close() error {
err := b.ReadCloser.Close()
b.cancel()
return err
}
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
return fallback.roundTrip(request, true)
+6 -4
View File
@@ -25,10 +25,12 @@ type ManagedTransport struct {
factory func() (innerTransport, error)
cheapRebuild bool
dialer N.Dialer
headers http.Header
host string
tag string
dialer N.Dialer
headers http.Header
host string
tag string
detour string
defaultOutbound bool
}
type transportEpoch struct {
+29 -5
View File
@@ -9,11 +9,13 @@ import (
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
)
var (
_ adapter.HTTPClientManager = (*Manager)(nil)
_ adapter.LifecycleService = (*Manager)(nil)
_ adapter.Referrer = (*Manager)(nil)
)
type Manager struct {
@@ -59,7 +61,7 @@ func (m *Manager) Name() string {
return "http-client"
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
@@ -70,6 +72,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
}
m.defaultTransport = sharedTransport
}
scope.Add(m.close)
return nil
}
@@ -161,12 +164,9 @@ func (m *Manager) ResetNetwork() {
}
}
func (m *Manager) Close() error {
func (m *Manager) close() error {
m.access.Lock()
defer m.access.Unlock()
if m.managedTransports == nil {
return nil
}
var err error
for _, transport := range m.managedTransports {
err = E.Append(err, transport.close(), func(err error) error {
@@ -177,3 +177,27 @@ func (m *Manager) Close() error {
m.sharedTransports = nil
return err
}
func (m *Manager) References() []string {
m.access.Lock()
defer m.access.Unlock()
var references []string
var needDefault bool
for _, transport := range m.managedTransports {
if transport.detour != "" {
references = append(references, transport.detour)
} else if transport.defaultOutbound {
needDefault = true
}
}
if needDefault {
outboundManager := service.FromContext[adapter.OutboundManager](m.ctx)
if outboundManager != nil {
defaultOutbound := outboundManager.Default()
if defaultOutbound != nil {
references = append(references, defaultOutbound.Tag())
}
}
}
return references
}
+6 -20
View File
@@ -8,26 +8,12 @@ import (
"github.com/sagernet/sing/common/x/list"
)
/*type GroupedConn interface {
MarkAsInternal()
}
func MarkAsInternal(conn any) {
if groupedConn, isGroupConn := common.Cast[GroupedConn](conn); isGroupConn {
groupedConn.MarkAsInternal()
}
}*/
type Conn struct {
net.Conn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *Conn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func (c *Conn) Close() error {
c.group.access.Lock()
c.group.connections.Remove(c.element)
@@ -48,20 +34,20 @@ func (c *Conn) Upstream() any {
}
type PacketConn struct {
net.PacketConn
N.NetPacketConn
group *Group
element *list.Element[*groupConnItem]
}
/*func (c *PacketConn) MarkAsInternal() {
c.element.Value.internal = true
}*/
func newPacketConn(group *Group, conn net.PacketConn, element *list.Element[*groupConnItem]) *PacketConn {
return &PacketConn{NetPacketConn: bufio.NewPacketConn(conn), group: group, element: element}
}
func (c *PacketConn) Close() error {
c.group.access.Lock()
c.group.connections.Remove(c.element)
c.group.access.Unlock()
return c.PacketConn.Close()
return c.NetPacketConn.Close()
}
func (c *PacketConn) ReaderReplaceable() bool {
@@ -73,7 +59,7 @@ func (c *PacketConn) WriterReplaceable() bool {
}
func (c *PacketConn) Upstream() any {
return bufio.NewPacketConn(c.PacketConn)
return c.NetPacketConn
}
type SingPacketConn struct {
+9 -11
View File
@@ -35,7 +35,7 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool, isProvider b
g.access.Lock()
defer g.access.Unlock()
item := g.connections.PushBack(&groupConnItem{conn, isExternal, isProvider})
return &PacketConn{PacketConn: conn, group: g, element: item}
return newPacketConn(g, conn, item)
}
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider bool) N.PacketConn {
@@ -47,19 +47,17 @@ func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider
func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock()
var toDelete []*list.Element[*groupConnItem]
var toClose []io.Closer
for element := g.connections.Front(); element != nil; element = element.Next() {
var closers []io.Closer
for element := g.connections.Front(); element != nil; {
nextElement := element.Next()
if !element.Value.isExternal || interruptExternalConnections {
toDelete = append(toDelete, element)
toClose = append(toClose, element.Value.conn)
closers = append(closers, element.Value.conn)
g.connections.Remove(element)
}
}
for _, element := range toDelete {
g.connections.Remove(element)
element = nextElement
}
g.access.Unlock()
for _, conn := range toClose {
_ = conn.Close()
for _, closer := range closers {
closer.Close()
}
}
+13 -5
View File
@@ -264,16 +264,24 @@ func (j *ClientHello) parseExtensions(exs []byte) error {
return &ParseError{LengthErr, 19}
}
versionsLen := int(sex[0])
for i := 0; i < versionsLen; i += 2 {
versions = append(versions, binary.BigEndian.Uint16(sex[1:][i:]))
sex = sex[versionExtensionHeaderLen:]
if len(sex) < versionsLen {
return &ParseError{LengthErr, 21}
}
for i := range versionsLen / 2 {
versions = append(versions, binary.BigEndian.Uint16(sex[i*2:]))
}
case signatureAlgorithmsExtensionType:
if len(sex) < signatureAlgorithmsExtensionHeaderLen {
return &ParseError{LengthErr, 20}
}
ssaLen := binary.BigEndian.Uint16(sex)
for i := 0; i < int(ssaLen); i += 2 {
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[2:][i:]))
ssaLen := int(binary.BigEndian.Uint16(sex))
sex = sex[signatureAlgorithmsExtensionHeaderLen:]
if len(sex) < ssaLen {
return &ParseError{LengthErr, 22}
}
for i := range ssaLen / 2 {
signatureAlgorithms = append(signatureAlgorithms, binary.BigEndian.Uint16(sex[i*2:]))
}
}
exs = exs[4+exLen:]
+3 -3
View File
@@ -89,7 +89,7 @@ func (l *Listener) Start() error {
if common.Contains(l.network, N.NetworkUDP) {
_, err := l.ListenUDP()
if err != nil {
return err
return E.Errors(err, l.Close())
}
l.packetOutboundClosed = make(chan struct{})
l.packetOutbound = make(chan *N.PacketBuffer, 64)
@@ -109,11 +109,11 @@ func (l *Listener) Start() error {
}
systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS, nil)
if err != nil {
return E.Cause(err, "initialize system proxy")
return E.Errors(E.Cause(err, "initialize system proxy"), l.Close())
}
err = systemProxy.Enable()
if err != nil {
return E.Errors(E.Cause(err, "set system proxy"), systemProxy.Close())
return E.Errors(E.Cause(err, "set system proxy"), systemProxy.Close(), l.Close())
}
l.systemProxy = systemProxy
}
+17 -7
View File
@@ -84,22 +84,32 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
func (l *Listener) loopTCPIn() {
tcpListener := l.tcpListener
var metadata adapter.InboundContext
var (
metadata adapter.InboundContext
acceptDelay time.Duration
)
for {
conn, err := tcpListener.Accept()
if err != nil {
//nolint:staticcheck
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
l.logger.Error(err)
continue
}
if l.shutdown.Load() && E.IsClosed(err) {
return
}
//nolint:staticcheck
if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() {
if acceptDelay == 0 {
acceptDelay = 5 * time.Millisecond
} else {
acceptDelay = min(2*acceptDelay, time.Second)
}
l.logger.Error(err, ", retrying in ", acceptDelay)
time.Sleep(acceptDelay)
continue
}
l.tcpListener.Close()
l.logger.Error("tcp listener closed: ", err)
continue
return
}
acceptDelay = 0
//nolint:staticcheck
metadata.InboundDetour = l.listenOptions.Detour
metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap()
+3 -1
View File
@@ -42,7 +42,9 @@ func (l *Listener) ListenUDPWithConfig(listenConfig net.ListenConfig) (net.Packe
} else {
udpFragment = l.listenOptions.UDPFragmentDefault
}
if !udpFragment {
if udpFragment {
listenConfig.Control = control.Append(listenConfig.Control, control.EnableUDPFragment())
} else {
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
}
if l.tproxy {
+2 -2
View File
@@ -51,9 +51,9 @@ type clientDialer struct {
}
func (d *clientDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
return d.Dialer.DialContext(adapter.OverrideContext(ctx), network, destination)
return d.Dialer.DialContext(adapter.ContextForMultiplexSession(ctx), network, destination)
}
func (d *clientDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.Dialer.ListenPacket(adapter.OverrideContext(ctx), destination)
return d.Dialer.ListenPacket(adapter.ContextForMultiplexSession(ctx), destination)
}
+10 -16
View File
@@ -23,7 +23,6 @@ type Manager struct {
namespaces []option.NetworkNamespace
holderArgs []string
paths map[string]string
holders []*holder
}
type holder struct {
@@ -64,7 +63,7 @@ func (m *Manager) Name() string {
return "netns"
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateInitialize {
return nil
}
@@ -80,7 +79,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
return E.Cause(err, "network namespace[", namespace.Tag, "]")
}
case C.NetNsTypeUnshare:
err := m.startNamespace(namespace)
err := m.startNamespace(namespace, scope)
if err != nil {
return E.Cause(err, "network namespace[", namespace.Tag, "]")
}
@@ -89,7 +88,7 @@ func (m *Manager) Start(stage adapter.StartStage) error {
return nil
}
func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
func (m *Manager) startNamespace(namespace option.NetworkNamespace, scope *adapter.Scope) error {
if len(m.holderArgs) == 0 {
return E.New("unshare network namespace is only supported in `sing-box run`")
}
@@ -97,7 +96,13 @@ func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
if err != nil {
return err
}
m.holders = append(m.holders, created)
scope.Add(func() error {
created.pipeWriter.Close()
if created.pidFile != "" {
os.Remove(created.pidFile)
}
return nil
})
pid := created.command.Process.Pid
if created.pidFile != "" {
err = os.WriteFile(created.pidFile, []byte(strconv.Itoa(pid)+"\n"), 0o644)
@@ -115,17 +120,6 @@ func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
return nil
}
func (m *Manager) Close() error {
for _, created := range m.holders {
created.pipeWriter.Close()
if created.pidFile != "" {
os.Remove(created.pidFile)
}
}
m.holders = nil
return nil
}
func (m *Manager) ResolvePath(nameOrPath string) string {
path, loaded := m.paths[nameOrPath]
if loaded && path != "" {
+1 -5
View File
@@ -22,11 +22,7 @@ func (m *Manager) Name() string {
return "netns"
}
func (m *Manager) Start(stage adapter.StartStage) error {
return nil
}
func (m *Manager) Close() error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
+5 -3
View File
@@ -4,6 +4,7 @@ package netns
import (
"bufio"
"context"
"os"
"strconv"
"strings"
@@ -12,6 +13,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/logger"
@@ -39,11 +41,11 @@ func TestUnshareNamespace(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = manager.Start(adapter.StartStateInitialize)
scope := adapter.NewScope(context.Background(), log.NewNOPFactory().Logger())
err = manager.Start(adapter.StartStateInitialize, scope)
if err != nil {
t.Fatal(err)
}
defer manager.Close()
pipeReader.SetReadDeadline(time.Now().Add(10 * time.Second))
pidLine, err := bufio.NewReader(pipeReader).ReadString('\n')
@@ -71,7 +73,7 @@ func TestUnshareNamespace(t *testing.T) {
t.Fatal("holder is in the current netns")
}
err = manager.Close()
err = scope.Close()
if err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -23,6 +23,6 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
}
}
metadata.Protocol = C.ProtocolHTTP
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
metadata.Domain = M.ParseSocksaddr(request.Host).Fqdn
return nil
}
+4 -1
View File
@@ -245,6 +245,9 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
if err != nil {
return err
}
if length > uint64(decryptedReader.Len()) {
return os.ErrInvalid
}
index := len(decrypted) - decryptedReader.Len()
fragments = append(fragments, qCryptoFragment{offset, length, decrypted[index : index+int(length)]})
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
@@ -291,7 +294,7 @@ func QUICClientHello(ctx context.Context, metadata *adapter.InboundContext, pack
find:
for {
for _, fragment := range fragments {
if fragment.offset == index {
if fragment.offset == index && fragment.length > 0 {
buffer.Write(fragment.payload)
index = fragment.offset + fragment.length
length++
+6 -1
View File
@@ -49,9 +49,14 @@ func (w *acmeWrapper) Start() error {
Logger: w.zapLogger,
})
config = certmagic.New(cache, *config)
err := config.ManageSync(w.ctx, w.domain)
if err != nil {
cache.Stop()
return err
}
w.cfg = config
w.cache = cache
return w.cfg.ManageSync(w.ctx, w.domain)
return nil
}
func (w *acmeWrapper) Close() error {
+1 -1
View File
@@ -347,5 +347,5 @@ func (c *realityClientConnWrapper) ReaderReplaceable() bool {
}
func (c *realityClientConnWrapper) WriterReplaceable() bool {
return true
return false
}
+1 -1
View File
@@ -254,5 +254,5 @@ func (c *realityConnWrapper) ReaderReplaceable() bool {
}
func (c *realityConnWrapper) WriterReplaceable() bool {
return true
return false
}
+10 -3
View File
@@ -57,21 +57,26 @@ func (p *sharedCertificateProvider) GetACMENextProtos() []string {
}
type inlineCertificateProvider struct {
ctx context.Context
logger log.ContextLogger
provider adapter.CertificateProviderService
scope *adapter.Scope
}
func (p *inlineCertificateProvider) Start() error {
p.scope = adapter.NewScope(p.ctx, p.logger)
name := "certificate-provider/" + p.provider.Type()
for _, stage := range adapter.ListStartStages {
err := adapter.LegacyStart(p.provider, stage)
err := p.scope.Start(name, p.provider, stage)
if err != nil {
return err
return E.Errors(err, p.scope.Close())
}
}
return nil
}
func (p *inlineCertificateProvider) Close() error {
return p.provider.Close()
return p.scope.Close()
}
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
@@ -548,6 +553,8 @@ func newCertificateProvider(ctx context.Context, logger log.ContextLogger, optio
return nil, E.Cause(err, "create inline certificate provider")
}
return &inlineCertificateProvider{
ctx: ctx,
logger: logger,
provider: provider,
}, nil
}
+1 -1
View File
@@ -41,7 +41,7 @@ func NewConn(conn net.Conn, ctx context.Context, splitPacket bool, splitRecord b
}
func (c *Conn) Write(b []byte) (n int, err error) {
if !c.firstPacketWritten {
if !c.firstPacketWritten && len(b) > 0 {
defer func() {
c.firstPacketWritten = true
}()
+7 -13
View File
@@ -45,7 +45,6 @@ type Manager struct {
eventSubscriber *observable.Subscriber[ConnectionEvent]
eventObserver *observable.Observer[ConnectionEvent]
cleaner *cleanup.Cleaner
}
func NewManager(outbound adapter.OutboundManager) *Manager {
@@ -59,20 +58,15 @@ func (m *Manager) Name() string {
return "traffic manager"
}
func (m *Manager) Start(stage adapter.StartStage) error {
func (m *Manager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage == adapter.StartStateInitialize {
m.eventObserver = observable.NewObserver(m.eventSubscriber, 64)
m.cleaner = cleanup.Add(m.Clear)
}
return nil
}
func (m *Manager) Close() error {
if m.cleaner != nil {
m.cleaner.Close()
}
if m.eventObserver != nil {
return m.eventObserver.Close()
scope.Add(m.eventObserver.Close)
cleaner := cleanup.Add(m.Clear)
scope.Add(func() error {
cleaner.Close()
return nil
})
}
return nil
}
+13
View File
@@ -0,0 +1,13 @@
//go:build with_quic
package urltest
import (
"context"
"github.com/sagernet/sing-quic"
)
func contextWithQUICKeepSession(ctx context.Context) context.Context {
return qtls.ContextWithKeepSession(ctx)
}
+9
View File
@@ -0,0 +1,9 @@
//go:build !with_quic
package urltest
import "context"
func contextWithQUICKeepSession(ctx context.Context) context.Context {
return ctx
}
+7 -1
View File
@@ -11,6 +11,8 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-snell"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -81,7 +83,11 @@ func (s *HistoryStorage) Close() error {
func URLTest(ctx context.Context, link string, detour N.Dialer) (uint16, error) {
multiplexOutbound, isMultiplexOutbound := common.Cast[adapter.OutboundWithMultiplex](detour)
if isMultiplexOutbound && multiplexOutbound.MultiplexEnabled() {
_, err := urlTest(ctx, link, detour)
warmContext := adapter.ContextWithKeepSession(ctx)
warmContext = mux.ContextWithKeepSession(warmContext)
warmContext = contextWithQUICKeepSession(warmContext)
warmContext = snell.ContextWithKeepSession(warmContext)
_, err := urlTest(warmContext, link, detour)
if err != nil {
return 0, err
}
+5
View File
@@ -0,0 +1,5 @@
//go:build !(ios && tvos)
package constant
const IsTvOS = false
+5
View File
@@ -0,0 +1,5 @@
//go:build ios && tvos
package constant
const IsTvOS = true
+29 -1
View File
@@ -14,9 +14,11 @@ import (
"github.com/sagernet/sing-box/experimental/locale"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/powerreport"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
@@ -30,6 +32,7 @@ type Instance struct {
trafficManager *trafficcontrol.Manager
cacheFile adapter.CacheFile
pauseManager pause.Manager
pauseCallback *list.Element[pause.Callback]
urlTestHistoryStorage *urltest.HistoryStorage
outboundManager adapter.OutboundManager
endpointManager adapter.EndpointManager
@@ -79,7 +82,7 @@ type OverrideOptions struct {
ExcludePackage []string
}
func (s *StartedService) newInstance(ctx context.Context, profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
func (s *StartedService) newInstance(ctx context.Context, profileContent string, overrideOptions *OverrideOptions, reloading bool) (*Instance, error) {
selectedLocale := locale.FromContext(ctx)
ctx, _ = locale.ContextWithLocale(s.ctx, selectedLocale.Locale)
ctx = service.ExtendContext(ctx)
@@ -139,6 +142,7 @@ func (s *StartedService) newInstance(ctx context.Context, profileContent string,
i.clashMode = service.PtrFromContext[clashmode.Manager](ctx)
i.trafficManager = service.PtrFromContext[trafficcontrol.Manager](ctx)
i.pauseManager = service.FromContext[pause.Manager](ctx)
i.registerPowerReport(ctx, reloading)
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
i.outboundManager = service.FromContext[adapter.OutboundManager](ctx)
i.endpointManager = service.FromContext[adapter.EndpointManager](ctx)
@@ -168,10 +172,34 @@ func (i *Instance) Start() error {
func (i *Instance) Close() error {
i.cancel()
if i.pauseCallback != nil {
i.pauseManager.UnregisterCallback(i.pauseCallback)
i.pauseCallback = nil
}
i.urlTestHistoryStorage.Close()
return i.instance.Close()
}
func (i *Instance) registerPowerReport(ctx context.Context, reloading bool) {
powerManager := service.FromContext[*powerreport.Manager](ctx)
if powerManager == nil {
return
}
recorder := powerManager.Recorder()
if recorder != nil {
recorder.RecordServiceStart(i.instance.CreatedAt(), reloading)
}
if i.pauseManager == nil {
return
}
i.pauseCallback = i.pauseManager.RegisterCallback(func(event int) {
recorder := powerManager.Recorder()
if recorder != nil {
recorder.RecordPauseEvent(event)
}
})
}
func (i *Instance) Box() *box.Box {
return i.instance
}
+10 -2
View File
@@ -20,6 +20,7 @@ import (
"github.com/sagernet/sing-box/experimental/locale"
"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/memory"
"github.com/sagernet/sing/common/observable"
@@ -259,15 +260,22 @@ func (s *StartedService) StartOrReloadService(ctx context.Context, profileConten
s.instance = nil
s.updateStatus(ServiceStatus_STOPPING)
s.serviceAccess.Unlock()
oomRecorder := service.FromContext[*oomkiller.Recorder](s.ctx)
if oomRecorder != nil {
oomRecorder.BeginReload()
defer oomRecorder.EndReload()
}
_ = oldInstance.Close()
runtimeDebug.FreeOSMemory()
s.serviceAccess.Lock()
}
s.startInterrupted = false
s.updateStatus(ServiceStatus_STARTING)
s.resetLogs()
if oldInstance == nil {
s.resetLogs()
}
s.serviceAccess.Unlock()
instance, err := s.newInstance(ctx, profileContent, options)
instance, err := s.newInstance(ctx, profileContent, options, oldInstance != nil)
if err != nil {
s.serviceAccess.Lock()
s.updateStatusError(err)
+67 -39
View File
@@ -48,7 +48,7 @@ type Client struct {
networkManager adapter.NetworkManager
logger logger.ContextLogger
cache *freelru.Cache[dnsCacheKey, *dns.Msg]
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
cacheLock compatible.Map[dnsExchangeKey, chan struct{}]
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
}
@@ -95,6 +95,11 @@ type dnsCacheKey struct {
environment uint64
}
type dnsExchangeKey struct {
dnsCacheKey
timeout time.Duration
}
func (k dnsCacheKey) persistentName() string {
name := k.transportTag
if k.clientSubnet.IsValid() {
@@ -251,6 +256,10 @@ func (o *exchangeOperation) release() {
}
func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool, allowWait bool) (*exchangeOperation, *dns.Msg, exchangeStatus, error) {
err := ctx.Err()
if err != nil {
return nil, nil, exchangeDone, err
}
transportStack := transportStackFromContext(ctx)
if containsTransport(transportStack, transport.Tag()) {
return nil, nil, exchangeDone, E.New("DNS resolution loop detected: ", formatTransportLoop(transportStack, transport.Tag()))
@@ -291,8 +300,33 @@ func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTranspo
if !disableCache {
cacheKey := c.newCacheKey(transport, question, message, options)
operation.cacheKey = cacheKey
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
if loaded {
exchangeKey := dnsExchangeKey{dnsCacheKey: cacheKey, timeout: options.Timeout}
cond, loaded := c.cacheLock.LoadOrStore(exchangeKey, make(chan struct{}))
if !loaded {
operation.releaseCond = func() {
c.cacheLock.Delete(exchangeKey)
close(cond)
}
}
for {
response, ttl, isStale := c.loadResponse(cacheKey)
if response != nil {
if isStale && !options.DisableOptimisticCache {
c.backgroundRefreshDNS(transport, cacheKey, message.Copy(), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
response.Id = message.Id
operation.release()
return nil, response, exchangeDone, nil
} else if !isStale {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
operation.release()
return nil, response, exchangeDone, nil
}
}
if !loaded {
break
}
if !allowWait {
return nil, nil, exchangeWait, nil
}
@@ -301,28 +335,13 @@ func (c *Client) beginExchange(ctx context.Context, transport adapter.DNSTranspo
case <-ctx.Done():
return nil, nil, exchangeDone, ctx.Err()
}
err = ctx.Err()
if err != nil {
return nil, nil, exchangeDone, err
}
cacheKey = c.newCacheKey(transport, question, message, options)
operation.cacheKey = cacheKey
} else {
operation.releaseCond = func() {
c.cacheLock.Delete(cacheKey)
close(cond)
}
}
response, ttl, isStale := c.loadResponse(cacheKey)
if response != nil {
if isStale && !options.DisableOptimisticCache {
c.backgroundRefreshDNS(transport, cacheKey, message.Copy(), options, responseChecker)
logOptimisticResponse(c.logger, ctx, response)
response.Id = message.Id
operation.release()
return nil, response, exchangeDone, nil
} else if !isStale {
logCachedResponse(c.logger, ctx, response, ttl)
response.Id = message.Id
operation.release()
return nil, response, exchangeDone, nil
}
loaded = false
}
}
@@ -379,12 +398,17 @@ func (c *Client) finishExchange(transport adapter.DNSTransport, operation *excha
}
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 options.Timeout == 0 {
options.Timeout = c.timeout
}
ctx, cancel := context.WithTimeout(ctx, options.Timeout)
defer cancel()
operation, earlyResponse, status, err := c.beginExchange(ctx, transport, message, options, responseChecker, true)
if status != exchangeReady {
return earlyResponse, err
}
defer operation.release()
response, err := c.exchangeToTransport(operation.ctx, transport, operation.message, options.Timeout)
response, err := c.exchangeToTransport(operation.ctx, transport, operation.message)
if err != nil {
return nil, err
}
@@ -392,18 +416,26 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
func (c *Client) ExchangeAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool, callback func(response *dns.Msg, err error)) {
if options.Timeout == 0 {
options.Timeout = c.timeout
}
ctx, cancel := context.WithTimeout(ctx, options.Timeout)
operation, earlyResponse, status, err := c.beginExchange(ctx, transport, message, options, responseChecker, false)
switch status {
case exchangeDone:
cancel()
callback(earlyResponse, err)
return
case exchangeWait:
go func() {
callback(c.Exchange(ctx, transport, message, options, responseChecker))
response, exchangeErr := c.Exchange(ctx, transport, message, options, responseChecker)
cancel()
callback(response, exchangeErr)
}()
return
}
finish := func(response *dns.Msg, exchangeErr error) {
cancel()
if exchangeErr != nil {
operation.release()
callback(nil, exchangeErr)
@@ -413,7 +445,7 @@ func (c *Client) ExchangeAsync(ctx context.Context, transport adapter.DNSTranspo
operation.release()
callback(finishedResponse, finishErr)
}
c.exchangeToTransportAsync(operation.ctx, transport, operation.message, options.Timeout, finish)
c.exchangeToTransportAsync(operation.ctx, transport, operation.message, finish)
}
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
@@ -644,8 +676,14 @@ func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, key dnsCac
}
go func() {
defer c.backgroundRefresh.Delete(key)
timeout := options.Timeout
if timeout == 0 {
timeout = c.timeout
}
ctx := adapter.ContextWithDNSTransportTag(c.ctx, transport.Tag())
response, err := c.exchangeToTransport(ctx, transport, message, options.Timeout)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
response, err := c.exchangeToTransport(ctx, transport, message)
if err != nil {
if c.logger != nil {
c.logger.DebugContext(ctx, "optimistic refresh failed for ", FqdnToDomain(key.Name), ": ", err)
@@ -718,12 +756,7 @@ func transportStackFromContext(ctx context.Context) []string {
return value
}
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, timeout time.Duration) (*dns.Msg, error) {
if timeout == 0 {
timeout = c.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
response, err := transport.Exchange(ctx, message)
if err == nil {
stripDNSPadding(response)
@@ -745,13 +778,8 @@ func containsTransport(stack []string, tag string) bool {
return false
}
func (c *Client) exchangeToTransportAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, timeout time.Duration, callback func(response *dns.Msg, err error)) {
if timeout == 0 {
timeout = c.timeout
}
ctx, cancel := context.WithTimeout(ctx, timeout)
func (c *Client) exchangeToTransportAsync(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, callback func(response *dns.Msg, err error)) {
transport.ExchangeAsync(ctx, message, func(response *dns.Msg, err error) {
cancel()
if err == nil {
stripDNSPadding(response)
callback(response, nil)
+15 -16
View File
@@ -129,10 +129,19 @@ func (r *Router) Initialize(rules []option.DNSRule) error {
return nil
}
func (r *Router) Start(stage adapter.StartStage) error {
func (r *Router) Start(stage adapter.StartStage, scope *adapter.Scope) error {
monitor := taskmonitor.New(r.logger, C.StartTimeout)
switch stage {
case adapter.StartStateStart:
scope.Add(func() error {
r.rulesAccess.Lock()
r.closing = true
runtimeRules := r.rules
r.rules = nil
r.rulesAccess.Unlock()
closeRules(runtimeRules)
return nil
})
monitor.Start("initialize DNS client")
r.client.Start()
monitor.Finish()
@@ -163,20 +172,6 @@ func (r *Router) Start(stage adapter.StartStage) error {
return nil
}
func (r *Router) Close() error {
r.rulesAccess.Lock()
if r.closing {
r.rulesAccess.Unlock()
return nil
}
r.closing = true
runtimeRules := r.rules
r.rules = nil
r.rulesAccess.Unlock()
closeRules(runtimeRules)
return nil
}
func (r *Router) buildRules(startRules bool) ([]adapter.DNSRule, bool, dnsRuleModeFlags, error) {
for i, ruleOptions := range r.rawRules {
err := R.ValidateNoNestedDNSRuleActions(ruleOptions)
@@ -1059,7 +1054,11 @@ func (r *Router) prepareExchange(ctx context.Context, message *mDNS.Msg) (*dnsEx
if r.powerManager != nil {
recorder := r.powerManager.Recorder()
if recorder != nil {
recorder.CountDNSQuery()
var domain string
if len(message.Question) == 1 {
domain = message.Question[0].Name
}
recorder.CountDNSQuery(domain)
}
}
if len(message.Question) != 1 {
+2 -14
View File
@@ -31,11 +31,7 @@ type fakeDNSTransport struct {
firstQueried time.Time
}
func (t *fakeDNSTransport) Start(stage adapter.StartStage) error {
return nil
}
func (t *fakeDNSTransport) Close() error {
func (t *fakeDNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
@@ -90,11 +86,7 @@ type fakeDNSTransportManager struct {
defaultTransport adapter.DNSTransport
}
func (m *fakeDNSTransportManager) Start(stage adapter.StartStage) error {
return nil
}
func (m *fakeDNSTransportManager) Close() error {
func (m *fakeDNSTransportManager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
@@ -115,10 +107,6 @@ func (m *fakeDNSTransportManager) FakeIP() adapter.FakeIPTransport {
return nil
}
func (m *fakeDNSTransportManager) Remove(tag string) error {
return nil
}
func (m *fakeDNSTransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) error {
return nil
}
+31
View File
@@ -47,6 +47,8 @@ type connPoolState[T comparable] struct {
shared T
hasShared bool
sharedClaimed bool
sharedUsers int
sharedWaiters int
sharedCtx context.Context
sharedCancel context.CancelCauseFunc
@@ -122,6 +124,9 @@ func (p *ConnPool[T]) Release(conn T, reuse bool) {
p.options.Close(conn, net.ErrClosed)
return
}
if p.options.Mode == ConnPoolSingle && state.hasShared && state.shared == conn && state.sharedUsers > 0 {
state.sharedUsers--
}
if p.options.Mode == ConnPoolOrdered {
if _, idle := state.idleElements[conn]; !idle {
state.idleElements[conn] = state.idle.PushBack(conn)
@@ -182,6 +187,7 @@ func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) {
state.shared = zero
state.hasShared = false
state.sharedClaimed = false
state.sharedUsers = 0
state.sharedCtx = nil
if state.sharedCancel != nil {
state.sharedCancel(cause)
@@ -196,6 +202,23 @@ func (p *ConnPool[T]) removeConn(state *connPoolState[T], conn T, cause error) {
}
}
func (p *ConnPool[T]) CloseIdle() {
p.access.Lock()
if p.closed {
p.access.Unlock()
return
}
state := p.state
if p.options.Mode != ConnPoolSingle || !state.hasShared || state.sharedUsers > 0 || state.sharedWaiters > 0 {
p.access.Unlock()
return
}
conn := state.shared
p.removeConn(state, conn, net.ErrClosed)
p.access.Unlock()
p.options.Close(conn, net.ErrClosed)
}
func (p *ConnPool[T]) Reset() {
p.access.Lock()
if p.closed {
@@ -309,6 +332,7 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
if p.options.IsAlive(conn) {
created := !current.sharedClaimed
current.sharedClaimed = true
current.sharedUsers++
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, nil
@@ -324,6 +348,7 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
current.connecting = &connPoolConnect[T]{done: make(chan struct{})}
}
state := current.connecting
current.sharedWaiters++
p.access.Unlock()
if startDial {
@@ -338,9 +363,13 @@ func (p *ConnPool[T]) acquireShared(ctx context.Context, dial func(context.Conte
}
return conn, connCtx, created, err
case <-ctx.Done():
p.access.Lock()
current.sharedWaiters--
p.access.Unlock()
return zero, nil, false, ctx.Err()
case <-current.ctx.Done():
p.access.Lock()
current.sharedWaiters--
closed := p.closed
p.access.Unlock()
if closed {
@@ -400,6 +429,7 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo
var zero T
p.access.Lock()
current.sharedWaiters--
if state.err != nil {
err := state.err
p.access.Unlock()
@@ -431,6 +461,7 @@ func (p *ConnPool[T]) collectShared(current *connPoolState[T], state *connPoolCo
created := !current.sharedClaimed
current.sharedClaimed = true
current.sharedUsers++
connCtx := current.sharedCtx
p.access.Unlock()
return conn, connCtx, created, false, nil
+29 -46
View File
@@ -18,7 +18,6 @@ import (
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
@@ -27,7 +26,6 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"github.com/insomniacslk/dhcp/dhcpv4"
@@ -54,7 +52,6 @@ type Transport struct {
networkManager adapter.NetworkManager
platformInterface adapter.PlatformInterface
interfaceName string
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
updateAccess sync.Mutex
updateCancel context.CancelFunc
refreshAccess sync.Mutex
@@ -70,6 +67,7 @@ type transportState struct {
search []string
servers []M.Socksaddr
serverTransports []adapter.DNSTransport
serverScope *adapter.Scope
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.DHCPDNSServerOptions) (adapter.DNSTransport, error) {
@@ -104,19 +102,29 @@ func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context,
}
}
func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(func() error {
t.Reset()
return nil
})
if t.interfaceName == "" {
interfaceMonitor := t.networkManager.InterfaceMonitor()
if interfaceMonitor == nil {
return E.New("missing monitor for auto DHCP, set route.auto_detect_interface")
}
t.interfaceCallback = interfaceMonitor.RegisterCallback(t.interfaceUpdated)
interfaceCallback := interfaceMonitor.RegisterCallback(func(defaultInterface *control.Interface, flags int) {
t.interfaceUpdated(scope.Context())
})
scope.Add(func() error {
interfaceMonitor.UnregisterCallback(interfaceCallback)
return nil
})
}
go func() {
err := t.fetch()
err := t.fetch(scope.Context())
if err != nil {
if errors.Is(err, errInterfaceIsCellular) && t.optional {
t.logger.Debug(E.Cause(errInterfaceIsCellular, "dhcp: fetch DNS servers"))
@@ -128,32 +136,12 @@ func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
if t.interfaceCallback != nil {
t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback)
}
t.updateAccess.Lock()
updateCancel := t.updateCancel
t.updateCancel = nil
t.updateAccess.Unlock()
if updateCancel != nil {
updateCancel()
}
t.refreshAccess.Lock()
defer t.refreshAccess.Unlock()
state := t.savedState.Swap(nil)
if state != nil {
closeServerTransports(state.serverTransports)
}
return nil
}
func (t *Transport) Reset() {
t.refreshAccess.Lock()
defer t.refreshAccess.Unlock()
state := t.savedState.Swap(nil)
if state != nil {
closeServerTransports(state.serverTransports)
if state != nil && state.serverScope != nil {
state.serverScope.Close()
}
}
@@ -169,12 +157,6 @@ func (t *Transport) Environment() []string {
return append(environment, state.search...)
}
func closeServerTransports(serverTransports []adapter.DNSTransport) {
for _, serverTransport := range serverTransports {
serverTransport.Close()
}
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
done := make(chan struct{})
var (
@@ -211,7 +193,7 @@ func (t *Transport) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callba
}
func (t *Transport) exchangeCold(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
err := t.fetch()
err := t.fetch(t.ctx)
if err != nil {
callback(nil, E.Cause(err, "dhcp: fetch DNS servers"))
return
@@ -235,7 +217,7 @@ func (t *Transport) Fetch() []M.Socksaddr {
return state.servers
}
func (t *Transport) fetch() error {
func (t *Transport) fetch(ctx context.Context) error {
state := t.savedState.Load()
if state != nil {
if state.lastError != nil {
@@ -256,7 +238,7 @@ func (t *Transport) fetch() error {
return nil
}
}
return t.updateServersLocked(t.ctx)
return t.updateServersLocked(ctx)
}
func (t *Transport) startRefresh() {
@@ -343,12 +325,13 @@ func (t *Transport) storeFailureLocked(err error) {
newState.search = previousState.search
newState.servers = previousState.servers
newState.serverTransports = previousState.serverTransports
newState.serverScope = previousState.serverScope
}
t.savedState.Store(newState)
}
func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) {
updateContext, updateCancel := context.WithCancel(t.ctx)
func (t *Transport) interfaceUpdated(ctx context.Context) {
updateContext, updateCancel := context.WithCancel(ctx)
t.updateAccess.Lock()
previousCancel := t.updateCancel
t.updateCancel = updateCancel
@@ -484,25 +467,25 @@ func (t *Transport) recreateServersLocked(iface *control.Interface, dhcpPacket *
}
if serversUnchanged && previousState.serverTransports != nil {
newState.serverTransports = previousState.serverTransports
newState.serverScope = previousState.serverScope
t.savedState.Store(newState)
return nil
}
serverScope := adapter.NewScope(t.ctx, t.logger)
serverTransports := make([]adapter.DNSTransport, 0, len(newState.servers))
for _, serverAddr := range newState.servers {
serverTransport := transport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), t.dialer, serverAddr)
err := serverTransport.Start(adapter.StartStateStart)
err := serverTransport.Start(adapter.StartStateStart, serverScope)
if err != nil {
for _, startedTransport := range serverTransports {
startedTransport.Close()
}
return E.Cause(err, "initialize transport for ", serverAddr)
return E.Errors(E.Cause(err, "initialize transport for ", serverAddr), serverScope.Close())
}
serverTransports = append(serverTransports, serverTransport)
}
newState.serverTransports = serverTransports
newState.serverScope = serverScope
t.savedState.Store(newState)
if previousState != nil {
closeServerTransports(previousState.serverTransports)
if previousState != nil && previousState.serverScope != nil {
previousState.serverScope.Close()
}
return nil
}
+64
View File
@@ -82,6 +82,70 @@ type sequentialCallState struct {
continued bool
}
func ExchangeParallel(ctx context.Context, exchangers []AsyncExchanger, accept func(response *mDNS.Msg, err error) bool, callback func(response *mDNS.Msg, err error)) {
if len(exchangers) == 0 {
callback(nil, E.New("missing exchangers"))
return
}
exchangeCtx, cancel := context.WithCancel(ctx)
parallel := &parallelExchange{
accept: accept,
callback: callback,
cancel: cancel,
responses: make([]*mDNS.Msg, len(exchangers)),
errors: make([]error, len(exchangers)),
pending: len(exchangers),
}
for i, exchanger := range exchangers {
exchanger(exchangeCtx, func(response *mDNS.Msg, err error) {
parallel.complete(i, response, err)
})
}
}
type parallelExchange struct {
access sync.Mutex
accept func(response *mDNS.Msg, err error) bool
callback func(response *mDNS.Msg, err error)
cancel context.CancelFunc
responses []*mDNS.Msg
errors []error
pending int
completed bool
}
func (p *parallelExchange) complete(index int, response *mDNS.Msg, err error) {
p.access.Lock()
if p.completed {
p.access.Unlock()
return
}
accepted := p.accept(response, err)
if !accepted {
p.responses[index] = response
p.errors[index] = err
p.pending--
if p.pending > 0 {
p.access.Unlock()
return
}
}
p.completed = true
p.access.Unlock()
p.cancel()
if accepted {
p.callback(response, err)
return
}
for i, finalResponse := range p.responses {
if p.errors[i] == nil {
p.callback(finalResponse, nil)
return
}
}
p.callback(nil, E.Errors(p.errors...))
}
func ExchangeNames(ctx context.Context, names []string, question mDNS.Question, exchangerFor func(fqdn string) AsyncExchanger, callback func(response *mDNS.Msg, err error)) {
if len(names) == 0 {
callback(nil, E.New("missing name candidates"))
+12 -7
View File
@@ -45,15 +45,16 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
return t.store.Start()
}
func (t *Transport) Close() error {
return t.store.Close()
err := t.store.Start()
if err != nil {
return err
}
scope.Add(t.store.Close)
return nil
}
func (t *Transport) Reset() {
@@ -67,7 +68,11 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg,
if question.Qtype == mDNS.TypeA && !t.inet4Enabled || question.Qtype == mDNS.TypeAAAA && !t.inet6Enabled {
return dns.FixedResponseStatus(message, mDNS.RcodeSuccess), nil
}
address, err := t.store.Create(dns.FqdnToDomain(question.Name), question.Qtype == mDNS.TypeAAAA)
domain := dns.FqdnToDomain(question.Name)
if len(domain) > 255 {
return nil, E.New("domain name too long for fakeip")
}
address, err := t.store.Create(domain, question.Qtype == mDNS.TypeAAAA)
if err != nil {
return nil, err
}
+1 -5
View File
@@ -56,11 +56,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
+1 -5
View File
@@ -58,11 +58,7 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
+29 -8
View File
@@ -34,7 +34,10 @@ import (
const MimeType = "application/dns-message"
var _ adapter.DNSTransport = (*HTTPSTransport)(nil)
var (
_ adapter.DNSTransport = (*HTTPSTransport)(nil)
_ adapter.IdleConnectionKeeper = (*HTTPSTransport)(nil)
)
func RegisterHTTPS(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteHTTPSDNSServerOptions](registry, C.DNSTypeHTTPS, NewHTTPS)
@@ -48,6 +51,7 @@ type HTTPSTransport struct {
headers http.Header
serverAddr M.Socksaddr
fallback *atomic.Bool
keepIdle atomic.Bool
transportAccess sync.Mutex
transport *HTTPSTransportWrapper
transportResetAt time.Time
@@ -131,7 +135,7 @@ func NewHTTPSRaw(
// plain HTTP DoH used by Tailscale
fallback.Store(true)
}
return &HTTPSTransport{
transport := &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
@@ -141,26 +145,40 @@ func NewHTTPSRaw(
fallback: fallback,
transport: NewHTTPSTransportWrapper(dialer, serverAddr, fallback),
}
transport.keepIdle.Store(true)
return transport
}
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
func (t *HTTPSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(func() error {
t.Reset()
return nil
})
return dialer.InitializeDetour(t.dialer)
}
func (t *HTTPSTransport) Close() error {
t.Reset()
return nil
}
func (t *HTTPSTransport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.resetTransportLocked()
}
func (t *HTTPSTransport) SetKeepIdleConnections(keep bool) {
t.keepIdle.Store(keep)
if !keep {
t.CloseIdleConnections()
}
}
func (t *HTTPSTransport) CloseIdleConnections() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
t.transport.CloseIdleConnections()
}
func (t *HTTPSTransport) resetTransportLocked() {
oldTransport := t.transport
t.transport = NewHTTPSTransportWrapper(t.dialer, t.serverAddr, t.fallback)
@@ -171,6 +189,9 @@ func (t *HTTPSTransport) resetTransportLocked() {
func (t *HTTPSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
startAt := time.Now()
response, err := t.exchange(ctx, message)
if !t.keepIdle.Load() {
t.CloseIdleConnections()
}
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
t.transportAccess.Lock()
+4
View File
@@ -111,6 +111,10 @@ func (h *HTTPSTransportWrapper) Close() {
for _, trackedConn := range connections {
trackedConn.Conn.Close()
}
h.CloseIdleConnections()
}
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
h.http2Transport.CloseIdleConnections()
h.httpTransport.CloseIdleConnections()
}
+18 -12
View File
@@ -12,7 +12,6 @@ import (
"github.com/sagernet/sing-box/dns/transport/mdns"
"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"
N "github.com/sagernet/sing/common/network"
@@ -39,6 +38,7 @@ type Transport struct {
preferGo bool
resolved ResolvedResolver
mdnsTransport adapter.DNSTransport
mdnsScope *adapter.Scope
configSource *systemconfig.Source
system systemResolver
serverSet atomic.Pointer[localServerSet]
@@ -65,13 +65,15 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
t.preferredResolver.Start(stage)
switch stage {
case adapter.StartStateInitialize:
scope.Add(t.configSource.Close)
if !t.preferGo && isSystemdResolvedManaged() {
resolvedResolver, err := NewResolvedResolver(t.ctx, t.logger)
if err == nil {
scope.Add(resolvedResolver.Close)
err = resolvedResolver.Start()
if err == nil {
t.resolved = resolvedResolver
@@ -81,13 +83,26 @@ func (t *Transport) Start(stage adapter.StartStage) error {
}
}
case adapter.StartStateStart:
scope.Add(func() error {
t.system.close()
return nil
})
scope.Add(func() error {
serverSet := t.serverSet.Swap(nil)
if serverSet != nil {
return serverSet.serverScope.Close()
}
return nil
})
if !C.IsDarwin {
t.mdnsTransport = mdns.NewRawTransport(t.TransportAdapter, t.ctx, t.logger)
t.mdnsScope = adapter.NewScope(t.ctx, t.logger)
scope.Add(t.mdnsScope.Close)
}
fallthrough
default:
if t.mdnsTransport != nil {
err := t.mdnsTransport.Start(stage)
err := t.mdnsTransport.Start(stage, t.mdnsScope)
if err != nil {
return err
}
@@ -96,15 +111,6 @@ func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
serverSet := t.serverSet.Swap(nil)
if serverSet != nil {
serverSet.Close()
}
t.system.close()
return common.Close(t.resolved, t.mdnsTransport, t.configSource)
}
func (t *Transport) Reset() {
serverSet := t.serverSet.Load()
if serverSet != nil {
-1
View File
@@ -11,6 +11,5 @@ type ResolvedResolver interface {
Close() error
Reset()
Environment() []string
Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error)
ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error))
}
+416 -228
View File
@@ -6,9 +6,11 @@ import (
"errors"
"net/netip"
"os"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@@ -17,7 +19,6 @@ import (
"github.com/sagernet/sing-box/dns"
dnsTransport "github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
@@ -54,20 +55,51 @@ func isSystemdResolvedManaged() bool {
type DBusResolvedResolver struct {
ctx context.Context
logger logger.ContextLogger
interfaceFinder control.InterfaceFinder
interfaceMonitor tun.DefaultInterfaceMonitor
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
networkMonitor tun.NetworkUpdateMonitor
networkCallback *list.Element[tun.NetworkUpdateCallback]
systemBus *dbus.Conn
savedServerSet atomic.Pointer[resolvedServerSet]
updateAccess sync.Mutex
updateCancel context.CancelFunc
updateRunAccess sync.Mutex
closed bool
closeOnce sync.Once
}
type resolvedServerSet struct {
servers []resolvedServer
signature []string
scopes []*resolvedScope
signature []string
serverScope *adapter.Scope
}
// Match levels of dns_scope_good_domain() in systemd-resolved: a routing or search
// domain match yields its label count, and only scopes at the best level are queried.
const (
resolvedScopeNoMatch = -3
resolvedScopeLastResort = -2
resolvedScopeMaybe = -1
)
// systemd-resolved only signals changes of the DNS server list, while clients such as
// NetworkManager and systemd-networkd configure domains, default route and DNS over TLS
// of a link in separate calls right after it; link state changes are not signaled at all.
const resolvedUpdateDelay = time.Second
// SD_RESOLVED_DNS bit of org.freedesktop.resolve1.Link.ScopesMask, set when the link has a unicast DNS scope.
const resolvedScopesMaskDNS = 1 << 0
// TRANSACTION_UDP_TIMEOUT_USEC in systemd-resolved (resolved-timeouts.h), after which a transaction
// is resent to the next server of the scope.
const resolvedServerTimeout = 5 * time.Second
type resolvedScope struct {
domains []string
defaultRoute bool
fallback bool
servers []resolvedServer
currentServer atomic.Uint32
}
type resolvedServer struct {
@@ -75,14 +107,51 @@ type resolvedServer struct {
fallbackTransport adapter.DNSTransport
}
type resolvedScopeSpecification struct {
interfaceName string
dnsOverTLSMode string
domains []string
defaultRoute bool
fallback bool
servers []resolvedServerSpecification
}
type resolvedServerSpecification struct {
address netip.Addr
port uint16
serverName string
}
type resolvedManagerDNS struct {
InterfaceIndex int32
Family int32
Address []byte
}
type resolvedManagerDNSEx struct {
InterfaceIndex int32
Family int32
Address []byte
Port uint16
Name string
}
type resolvedManagerDomain struct {
InterfaceIndex int32
Domain string
RoutingOnly bool
}
type resolvedManagerServer struct {
interfaceIndex int32
address []byte
port uint16
serverName string
}
func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (ResolvedResolver, error) {
interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor()
networkManager := service.FromContext[adapter.NetworkManager](ctx)
interfaceMonitor := networkManager.InterfaceMonitor()
if interfaceMonitor == nil {
return nil, os.ErrInvalid
}
@@ -93,7 +162,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso
return &DBusResolvedResolver{
ctx: ctx,
logger: logger,
interfaceFinder: networkManager.InterfaceFinder(),
interfaceMonitor: interfaceMonitor,
networkMonitor: networkManager.NetworkMonitor(),
systemBus: systemBus,
}, nil
}
@@ -101,6 +172,9 @@ func NewResolvedResolver(ctx context.Context, logger logger.ContextLogger) (Reso
func (t *DBusResolvedResolver) Start() error {
t.updateStatus(t.ctx)
t.interfaceCallback = t.interfaceMonitor.RegisterCallback(t.updateDefaultInterface)
if t.networkMonitor != nil {
t.networkCallback = t.networkMonitor.RegisterCallback(t.postUpdateStatus)
}
err := t.systemBus.BusObject().AddMatchSignal(
"org.freedesktop.DBus",
"NameOwnerChanged",
@@ -124,29 +198,26 @@ func (t *DBusResolvedResolver) Start() error {
}
func (t *DBusResolvedResolver) Close() error {
t.updateAccess.Lock()
updateCancel := t.updateCancel
t.updateCancel = nil
t.updateAccess.Unlock()
if updateCancel != nil {
updateCancel()
}
t.updateRunAccess.Lock()
t.closed = true
serverSet := t.savedServerSet.Swap(nil)
t.updateRunAccess.Unlock()
var closeErr error
t.closeOnce.Do(func() {
t.updateAccess.Lock()
updateCancel := t.updateCancel
t.updateCancel = nil
t.updateAccess.Unlock()
if updateCancel != nil {
updateCancel()
}
t.updateRunAccess.Lock()
t.closed = true
serverSet := t.savedServerSet.Swap(nil)
t.updateRunAccess.Unlock()
if serverSet != nil {
closeErr = serverSet.Close()
}
if t.interfaceCallback != nil {
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
}
if t.systemBus != nil {
_ = t.systemBus.Close()
}
})
if serverSet != nil {
closeErr = serverSet.serverScope.Close()
}
t.interfaceMonitor.UnregisterCallback(t.interfaceCallback)
if t.networkCallback != nil {
t.networkMonitor.UnregisterCallback(t.networkCallback)
}
_ = t.systemBus.Close()
return closeErr
}
@@ -155,10 +226,12 @@ func (t *DBusResolvedResolver) Reset() {
if serverSet == nil {
return
}
for _, server := range serverSet.servers {
server.primaryTransport.Reset()
if server.fallbackTransport != nil {
server.fallbackTransport.Reset()
for _, scope := range serverSet.scopes {
for _, server := range scope.servers {
server.primaryTransport.Reset()
if server.fallbackTransport != nil {
server.fallbackTransport.Reset()
}
}
}
}
@@ -171,40 +244,20 @@ func (t *DBusResolvedResolver) Environment() []string {
return serverSet.signature
}
func (t *DBusResolvedResolver) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
var err error
serverSet, err = t.checkResolved(context.Background())
if err != nil {
return nil, err
}
previousServerSet := t.savedServerSet.Swap(serverSet)
if previousServerSet != nil {
_ = previousServerSet.Close()
}
}
response, err := t.exchangeServerSet(ctx, message, serverSet)
if err == nil {
return response, nil
}
t.updateStatus(t.ctx)
refreshedServerSet := t.savedServerSet.Load()
if refreshedServerSet == nil || refreshedServerSet == serverSet {
return nil, err
}
return t.exchangeServerSet(ctx, message, refreshedServerSet)
}
func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
serverSet := t.savedServerSet.Load()
if serverSet == nil {
go func() {
callback(t.Exchange(ctx, message))
err := t.updateStatus(t.ctx)
if err != nil {
callback(nil, err)
return
}
t.exchangeServerSet(ctx, message, t.savedServerSet.Load(), callback)
}()
return
}
t.exchangeServerSetAsync(ctx, message, serverSet, func(response *mDNS.Msg, err error) {
t.exchangeServerSet(ctx, message, serverSet, func(response *mDNS.Msg, err error) {
if err == nil {
callback(response, nil)
return
@@ -216,29 +269,114 @@ func (t *DBusResolvedResolver) ExchangeAsync(ctx context.Context, message *mDNS.
callback(nil, err)
return
}
t.exchangeServerSetAsync(ctx, message, refreshedServerSet, callback)
t.exchangeServerSet(ctx, message, refreshedServerSet, callback)
}()
})
}
func (t *DBusResolvedResolver) exchangeServerSetAsync(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) {
if len(serverSet.servers) == 0 {
callback(nil, E.New("link has no DNS servers configured"))
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet, callback func(response *mDNS.Msg, err error)) {
if serverSet == nil {
callback(nil, os.ErrClosed)
return
}
serverExchangers := make([]dnsTransport.AsyncExchanger, 0, len(serverSet.servers))
for _, server := range serverSet.servers {
scopes := serverSet.selectScopes(message.Question[0].Name)
if len(scopes) == 0 {
callback(nil, E.New("no appropriate name servers or networks for name found"))
return
}
scopeExchangers := common.Map(scopes, func(scope *resolvedScope) dnsTransport.AsyncExchanger {
return func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
scope.exchange(exchangeCtx, message, exchangeCallback)
}
})
dnsTransport.ExchangeParallel(ctx, scopeExchangers, acceptResolvedScopeResponse, callback)
}
func (s *resolvedScope) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
serverCount := uint32(len(s.servers))
currentServer := s.currentServer.Load()
serverExchangers := make([]dnsTransport.AsyncExchanger, 0, serverCount)
for i := range serverCount {
serverIndex := (currentServer + i) % serverCount
server := s.servers[serverIndex]
serverExchangers = append(serverExchangers, func(exchangeCtx context.Context, exchangeCallback func(response *mDNS.Msg, err error)) {
server.primaryTransport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, exchangeErr error) {
if exchangeErr != nil && server.fallbackTransport != nil {
server.fallbackTransport.ExchangeAsync(exchangeCtx, message, exchangeCallback)
return
server.exchange(exchangeCtx, message, func(response *mDNS.Msg, err error) {
if exchangeCtx.Err() == nil && !acceptResolvedServerResponse(response, err) {
s.currentServer.CompareAndSwap(serverIndex, (serverIndex+1)%serverCount)
}
exchangeCallback(response, exchangeErr)
exchangeCallback(response, err)
})
})
}
dnsTransport.ExchangeSequential(ctx, serverExchangers, nil, callback)
dnsTransport.ExchangeSequential(ctx, serverExchangers, acceptResolvedServerResponse, callback)
}
func (s *resolvedServer) exchange(ctx context.Context, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
exchangeResolvedTransport(ctx, s.primaryTransport, message, func(response *mDNS.Msg, err error) {
if err != nil && s.fallbackTransport != nil {
exchangeResolvedTransport(ctx, s.fallbackTransport, message, callback)
return
}
callback(response, err)
})
}
func exchangeResolvedTransport(ctx context.Context, transport adapter.DNSTransport, message *mDNS.Msg, callback func(response *mDNS.Msg, err error)) {
exchangeCtx, cancel := context.WithTimeout(ctx, resolvedServerTimeout)
transport.ExchangeAsync(exchangeCtx, message, func(response *mDNS.Msg, err error) {
cancel()
callback(response, err)
})
}
// systemd-resolved starts a query on every scope at the best match level at once
// (resolved-dns-query.c dns_query_go_scopes) and completes it with the first positive answer
// (dns_query_ready). Within a scope, SERVFAIL and timeouts move the scope to its next server, which
// later queries start from (resolved-dns-transaction.c dns_transaction_retry).
func acceptResolvedServerResponse(response *mDNS.Msg, err error) bool {
return err == nil && response.Rcode != mDNS.RcodeServerFailure
}
func acceptResolvedScopeResponse(response *mDNS.Msg, err error) bool {
return err == nil && response.Rcode == mDNS.RcodeSuccess
}
func (s *resolvedServerSet) selectScopes(name string) []*resolvedScope {
var (
bestMatch = resolvedScopeNoMatch
selectedScopes []*resolvedScope
)
for _, scope := range s.scopes {
match := scope.match(name)
if match == resolvedScopeNoMatch || match < bestMatch {
continue
}
if match > bestMatch {
bestMatch = match
selectedScopes = selectedScopes[:0]
}
selectedScopes = append(selectedScopes, scope)
}
return selectedScopes
}
func (s *resolvedScope) match(name string) int {
bestLabels := -1
for _, domain := range s.domains {
if mDNS.IsSubDomain(domain, name) {
bestLabels = max(bestLabels, mDNS.CountLabel(domain))
}
}
if bestLabels >= 0 {
return bestLabels
}
if !s.defaultRoute {
return resolvedScopeNoMatch
}
if s.fallback {
return resolvedScopeLastResort
}
return resolvedScopeMaybe
}
func (t *DBusResolvedResolver) loopUpdateStatus() {
@@ -275,26 +413,39 @@ func (t *DBusResolvedResolver) postUpdateStatus() {
}
go func() {
defer updateCancel()
timer := time.NewTimer(resolvedUpdateDelay)
defer timer.Stop()
select {
case <-timer.C:
case <-updateContext.Done():
return
}
t.updateStatus(updateContext)
}()
}
func (t *DBusResolvedResolver) updateStatus(ctx context.Context) {
func (t *DBusResolvedResolver) updateStatus(ctx context.Context) error {
t.updateRunAccess.Lock()
defer t.updateRunAccess.Unlock()
if t.closed || ctx.Err() != nil {
return
if t.closed {
return os.ErrClosed
}
if ctx.Err() != nil {
return ctx.Err()
}
serverSet, err := t.checkResolved(ctx)
if t.closed || ctx.Err() != nil {
if serverSet != nil {
_ = serverSet.Close()
_ = serverSet.serverScope.Close()
}
return
if t.closed {
return os.ErrClosed
}
return ctx.Err()
}
oldServerSet := t.savedServerSet.Swap(serverSet)
if oldServerSet != nil {
_ = oldServerSet.Close()
_ = oldServerSet.serverScope.Close()
}
if err != nil {
var dbusErr dbus.Error
@@ -304,34 +455,16 @@ func (t *DBusResolvedResolver) updateStatus(ctx context.Context) {
if oldServerSet != nil {
t.logger.Debug("systemd-resolved service is gone")
}
return
return err
} else if oldServerSet == nil {
t.logger.Debug("using systemd-resolved service as resolver")
}
}
func (t *DBusResolvedResolver) exchangeServerSet(ctx context.Context, message *mDNS.Msg, serverSet *resolvedServerSet) (*mDNS.Msg, error) {
if serverSet == nil || len(serverSet.servers) == 0 {
return nil, E.New("link has no DNS servers configured")
}
var lastError error
for _, server := range serverSet.servers {
response, err := server.primaryTransport.Exchange(ctx, message)
if err != nil && server.fallbackTransport != nil {
response, err = server.fallbackTransport.Exchange(ctx, message)
}
if err != nil {
lastError = err
continue
}
return response, nil
}
return nil, lastError
return nil
}
func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServerSet, error) {
dbusObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := dbusObject.(*dbus.Object).CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err
managerObject := t.systemBus.Object("org.freedesktop.resolve1", "/org/freedesktop/resolve1")
err := managerObject.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Err
if err != nil {
return nil, err
}
@@ -339,102 +472,173 @@ func (t *DBusResolvedResolver) checkResolved(ctx context.Context) (*resolvedServ
if defaultInterface == nil {
return nil, E.New("missing default interface")
}
call := dbusObject.(*dbus.Object).CallWithContext(
ctx,
"org.freedesktop.resolve1.Manager.GetLink",
0,
int32(defaultInterface.Index),
)
if call.Err != nil {
return nil, call.Err
}
var linkPath dbus.ObjectPath
err = call.Store(&linkPath)
managerProperties, err := loadResolvedProperties(ctx, managerObject, "org.freedesktop.resolve1.Manager")
if err != nil {
return nil, err
}
linkObject := t.systemBus.Object("org.freedesktop.resolve1", linkPath)
if linkObject == nil {
return nil, E.New("missing link object for default interface")
}
dnsOverTLSMode, err := loadResolvedLinkDNSOverTLS(linkObject)
servers, err := loadResolvedManagerServers(managerProperties, "DNS")
if err != nil {
return nil, err
}
err = ctx.Err()
var managerDomains []resolvedManagerDomain
err = storeResolvedProperty(managerProperties, "Domains", &managerDomains)
if err != nil {
return nil, err
}
linkDNSEx, err := loadResolvedLinkDNSEx(linkObject)
var dnsOverTLSMode string
err = storeResolvedProperty(managerProperties, "DNSOverTLS", &dnsOverTLSMode)
if err != nil {
return nil, err
}
err = ctx.Err()
if err != nil {
return nil, err
globalScope := resolvedScopeSpecification{
interfaceName: defaultInterface.Name,
dnsOverTLSMode: dnsOverTLSMode,
defaultRoute: true,
}
linkDNS, err := loadResolvedLinkDNS(linkObject)
if err != nil {
return nil, err
var linkScopes []resolvedScopeSpecification
linkScopeIndexes := make(map[int32]int)
for _, server := range servers {
if server.interfaceIndex == 0 {
serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server)
if loaded {
globalScope.servers = append(globalScope.servers, serverSpecification)
}
continue
}
scopeIndex, loaded := linkScopeIndexes[server.interfaceIndex]
if !loaded {
linkScope, linkLoaded, linkErr := t.loadResolvedLinkScope(ctx, managerObject, server.interfaceIndex)
if linkErr != nil {
return nil, linkErr
}
if !linkLoaded {
linkScopeIndexes[server.interfaceIndex] = -1
continue
}
scopeIndex = len(linkScopes)
linkScopeIndexes[server.interfaceIndex] = scopeIndex
linkScopes = append(linkScopes, linkScope)
} else if scopeIndex < 0 {
continue
}
serverSpecification, loaded := buildResolvedServerSpecification(linkScopes[scopeIndex].interfaceName, server)
if loaded {
linkScopes[scopeIndex].servers = append(linkScopes[scopeIndex].servers, serverSpecification)
}
}
if len(linkDNSEx) == 0 && len(linkDNS) == 0 {
for _, inbound := range service.FromContext[adapter.InboundManager](t.ctx).Inbounds() {
if inbound.Type() == C.TypeTun {
return nil, E.New("No appropriate name servers or networks for name found")
for _, domain := range managerDomains {
domainName := mDNS.Fqdn(domain.Domain)
if domain.InterfaceIndex == 0 {
globalScope.domains = append(globalScope.domains, domainName)
continue
}
scopeIndex, loaded := linkScopeIndexes[domain.InterfaceIndex]
if loaded && scopeIndex >= 0 {
linkScopes[scopeIndex].domains = append(linkScopes[scopeIndex].domains, domainName)
}
}
if len(globalScope.servers) == 0 && !slices.ContainsFunc(linkScopes, func(it resolvedScopeSpecification) bool {
return it.defaultRoute && len(it.servers) > 0
}) {
fallbackServers, fallbackErr := loadResolvedManagerServers(managerProperties, "FallbackDNS")
if fallbackErr != nil {
return nil, fallbackErr
}
for _, server := range fallbackServers {
serverSpecification, loaded := buildResolvedServerSpecification(globalScope.interfaceName, server)
if loaded {
globalScope.servers = append(globalScope.servers, serverSpecification)
}
}
return nil, E.New("link has no DNS servers configured")
globalScope.fallback = true
}
scopeSpecifications := common.Filter(append([]resolvedScopeSpecification{globalScope}, linkScopes...), func(it resolvedScopeSpecification) bool {
return len(it.servers) > 0
})
if len(scopeSpecifications) == 0 {
return nil, E.New("no DNS servers configured")
}
serverSet := &resolvedServerSet{
serverScope: adapter.NewScope(t.ctx, t.logger),
}
for _, scopeSpecification := range scopeSpecifications {
scope, createErr := t.createResolvedScope(serverSet.serverScope, scopeSpecification)
if createErr != nil {
_ = serverSet.serverScope.Close()
return nil, createErr
}
serverSet.scopes = append(serverSet.scopes, scope)
for _, serverSpecification := range scopeSpecification.servers {
serverSet.signature = append(serverSet.signature, scopeSpecification.interfaceName+"/"+M.SocksaddrFrom(serverSpecification.address, serverSpecification.port).String())
}
}
return serverSet, nil
}
func (t *DBusResolvedResolver) loadResolvedLinkScope(ctx context.Context, managerObject dbus.BusObject, interfaceIndex int32) (resolvedScopeSpecification, bool, error) {
linkInterface, err := t.interfaceFinder.ByIndex(int(interfaceIndex))
if err != nil || slices.Contains(t.interfaceMonitor.MyInterfaces(), linkInterface.Name) {
return resolvedScopeSpecification{}, false, nil
}
var linkPath dbus.ObjectPath
err = managerObject.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.GetLink", 0, interfaceIndex).Store(&linkPath)
if err != nil {
return resolvedScopeSpecification{}, false, err
}
linkProperties, err := loadResolvedProperties(ctx, t.systemBus.Object("org.freedesktop.resolve1", linkPath), "org.freedesktop.resolve1.Link")
if err != nil {
return resolvedScopeSpecification{}, false, err
}
var scopesMask uint64
err = storeResolvedProperty(linkProperties, "ScopesMask", &scopesMask)
if err != nil {
return resolvedScopeSpecification{}, false, err
}
if scopesMask&resolvedScopesMaskDNS == 0 {
return resolvedScopeSpecification{}, false, nil
}
linkScope := resolvedScopeSpecification{
interfaceName: linkInterface.Name,
}
err = storeResolvedProperty(linkProperties, "DNSOverTLS", &linkScope.dnsOverTLSMode)
if err != nil {
return resolvedScopeSpecification{}, false, err
}
err = storeResolvedProperty(linkProperties, "DefaultRoute", &linkScope.defaultRoute)
if err != nil {
return resolvedScopeSpecification{}, false, err
}
return linkScope, true, nil
}
func (t *DBusResolvedResolver) createResolvedScope(serverScope *adapter.Scope, scopeSpecification resolvedScopeSpecification) (*resolvedScope, error) {
scope := &resolvedScope{
domains: scopeSpecification.domains,
defaultRoute: scopeSpecification.defaultRoute,
fallback: scopeSpecification.fallback,
}
serverDialer, err := dialer.NewDefault(t.ctx, option.DialerOptions{
AbstractDialerOptions: option.AbstractDialerOptions{
BindInterface: defaultInterface.Name,
BindInterface: scopeSpecification.interfaceName,
UDPFragmentDefault: true,
},
})
if err != nil {
return nil, err
}
var serverSpecifications []resolvedServerSpecification
if len(linkDNSEx) > 0 {
for _, entry := range linkDNSEx {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, entry.Port, entry.Name)
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
} else {
for _, entry := range linkDNS {
serverSpecification, loaded := buildResolvedServerSpecification(defaultInterface.Name, entry.Address, 0, "")
if !loaded {
continue
}
serverSpecifications = append(serverSpecifications, serverSpecification)
}
}
if len(serverSpecifications) == 0 {
return nil, E.New("no valid DNS servers on link")
}
serverSet := &resolvedServerSet{
servers: make([]resolvedServer, 0, len(serverSpecifications)),
signature: common.Map(serverSpecifications, func(it resolvedServerSpecification) string {
return M.SocksaddrFrom(it.address, it.port).String()
}),
}
for _, serverSpecification := range serverSpecifications {
server, createErr := t.createResolvedServer(serverDialer, dnsOverTLSMode, serverSpecification)
for _, serverSpecification := range scopeSpecification.servers {
server, createErr := t.createResolvedServer(serverScope, serverDialer, scopeSpecification.dnsOverTLSMode, serverSpecification)
if createErr != nil {
_ = serverSet.Close()
return nil, createErr
}
serverSet.servers = append(serverSet.servers, server)
scope.servers = append(scope.servers, server)
}
return serverSet, nil
return scope, nil
}
func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
func (t *DBusResolvedResolver) createResolvedServer(serverScope *adapter.Scope, serverDialer N.Dialer, dnsOverTLSMode string, serverSpecification resolvedServerSpecification) (resolvedServer, error) {
if dnsOverTLSMode == "yes" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
@@ -443,13 +647,12 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
}, nil
}
if dnsOverTLSMode == "opportunistic" {
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, true)
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, true)
if err != nil {
return resolvedServer{}, err
}
fallbackTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
fallbackTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, false)
if err != nil {
_ = primaryTransport.Close()
return resolvedServer{}, err
}
return resolvedServer{
@@ -457,7 +660,7 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
fallbackTransport: fallbackTransport,
}, nil
}
primaryTransport, err := t.createResolvedTransport(serverDialer, serverSpecification, false)
primaryTransport, err := t.createResolvedTransport(serverScope, serverDialer, serverSpecification, false)
if err != nil {
return resolvedServer{}, err
}
@@ -466,7 +669,7 @@ func (t *DBusResolvedResolver) createResolvedServer(serverDialer N.Dialer, dnsOv
}, nil
}
func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
func (t *DBusResolvedResolver) createResolvedTransport(serverScope *adapter.Scope, serverDialer N.Dialer, serverSpecification resolvedServerSpecification, useTLS bool) (adapter.DNSTransport, error) {
serverAddress := M.SocksaddrFrom(serverSpecification.address, resolvedServerPort(serverSpecification.port, useTLS))
if useTLS {
tlsAddress := serverSpecification.address
@@ -485,35 +688,22 @@ func (t *DBusResolvedResolver) createResolvedTransport(serverDialer N.Dialer, se
return nil, err
}
serverTransport := dnsTransport.NewTLSRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeTLS, "", nil), serverDialer, serverAddress, tlsConfig)
err = serverTransport.Start(adapter.StartStateStart)
err = serverTransport.Start(adapter.StartStateStart, serverScope)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
serverTransport := dnsTransport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), serverDialer, serverAddress)
err := serverTransport.Start(adapter.StartStateStart)
err := serverTransport.Start(adapter.StartStateStart, serverScope)
if err != nil {
_ = serverTransport.Close()
return nil, err
}
return serverTransport, nil
}
func (s *resolvedServerSet) Close() error {
var errors []error
for _, server := range s.servers {
errors = append(errors, server.primaryTransport.Close())
if server.fallbackTransport != nil {
errors = append(errors, server.fallbackTransport.Close())
}
}
return E.Errors(errors...)
}
func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, port uint16, serverName string) (resolvedServerSpecification, bool) {
address, loaded := netip.AddrFromSlice(rawAddress)
func buildResolvedServerSpecification(interfaceName string, server resolvedManagerServer) (resolvedServerSpecification, bool) {
address, loaded := netip.AddrFromSlice(server.address)
if !loaded {
return resolvedServerSpecification{}, false
}
@@ -522,8 +712,8 @@ func buildResolvedServerSpecification(interfaceName string, rawAddress []byte, p
}
return resolvedServerSpecification{
address: address,
port: port,
serverName: serverName,
port: server.port,
serverName: server.serverName,
}, true
}
@@ -537,57 +727,55 @@ func resolvedServerPort(port uint16, useTLS bool) uint16 {
return 53
}
func loadResolvedLinkDNS(linkObject dbus.BusObject) ([]resolved.LinkDNS, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNS")
func loadResolvedProperties(ctx context.Context, object dbus.BusObject, interfaceName string) (map[string]dbus.Variant, error) {
var properties map[string]dbus.Variant
err := object.CallWithContext(ctx, "org.freedesktop.DBus.Properties.GetAll", 0, interfaceName).Store(&properties)
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
return nil, E.Cause(err, "load ", interfaceName, " properties")
}
var linkDNS []resolved.LinkDNS
err = dnsProperty.Store(&linkDNS)
if err != nil {
return nil, err
}
return linkDNS, nil
return properties, nil
}
func loadResolvedLinkDNSEx(linkObject dbus.BusObject) ([]resolved.LinkDNSEx, error) {
dnsProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSEx")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return nil, nil
}
return nil, err
func storeResolvedProperty(properties map[string]dbus.Variant, name string, value any) error {
property, loaded := properties[name]
if !loaded {
return nil
}
var linkDNSEx []resolved.LinkDNSEx
err = dnsProperty.Store(&linkDNSEx)
err := property.Store(value)
if err != nil {
return E.Cause(err, "parse resolved property ", name)
}
return nil
}
func loadResolvedManagerServers(properties map[string]dbus.Variant, name string) ([]resolvedManagerServer, error) {
_, loaded := properties[name+"Ex"]
if loaded {
var serversEx []resolvedManagerDNSEx
err := storeResolvedProperty(properties, name+"Ex", &serversEx)
if err != nil {
return nil, err
}
return common.Map(serversEx, func(it resolvedManagerDNSEx) resolvedManagerServer {
return resolvedManagerServer{
interfaceIndex: it.InterfaceIndex,
address: it.Address,
port: it.Port,
serverName: it.Name,
}
}), nil
}
var servers []resolvedManagerDNS
err := storeResolvedProperty(properties, name, &servers)
if err != nil {
return nil, err
}
return linkDNSEx, nil
}
func loadResolvedLinkDNSOverTLS(linkObject dbus.BusObject) (string, error) {
dnsOverTLSProperty, err := linkObject.GetProperty("org.freedesktop.resolve1.Link.DNSOverTLS")
if err != nil {
if isResolvedUnknownPropertyError(err) {
return "", nil
return common.Map(servers, func(it resolvedManagerDNS) resolvedManagerServer {
return resolvedManagerServer{
interfaceIndex: it.InterfaceIndex,
address: it.Address,
}
return "", err
}
var dnsOverTLSMode string
err = dnsOverTLSProperty.Store(&dnsOverTLSMode)
if err != nil {
return "", err
}
return dnsOverTLSMode, nil
}
func isResolvedUnknownPropertyError(err error) bool {
var dbusError dbus.Error
return errors.As(err, &dbusError) && dbusError.Name == "org.freedesktop.DBus.Error.UnknownProperty"
}), nil
}
func shouldUpdateResolvedServerSet(signal *dbus.Signal) bool {
+10 -16
View File
@@ -14,14 +14,9 @@ import (
)
type localServerSet struct {
config *systemconfig.Config
transports []adapter.DNSTransport
}
func (s *localServerSet) Close() {
for _, serverTransport := range s.transports {
serverTransport.Close()
}
config *systemconfig.Config
transports []adapter.DNSTransport
serverScope *adapter.Scope
}
func (t *Transport) serverSetFor(systemConfig *systemconfig.Config) (*localServerSet, error) {
@@ -35,6 +30,7 @@ func (t *Transport) serverSetFor(systemConfig *systemconfig.Config) (*localServe
if serverSet != nil && serverSet.config == systemConfig {
return serverSet, nil
}
serverScope := adapter.NewScope(t.ctx, t.logger)
transports := make([]adapter.DNSTransport, 0, len(systemConfig.Servers))
for _, serverAddr := range systemConfig.Servers {
var serverTransport adapter.DNSTransport
@@ -43,22 +39,20 @@ func (t *Transport) serverSetFor(systemConfig *systemconfig.Config) (*localServe
} else {
serverTransport = transport.NewUDPRaw(t.logger, dns.NewTransportAdapter(C.DNSTypeUDP, "", nil), t.dialer, serverAddr)
}
err := serverTransport.Start(adapter.StartStateStart)
err := serverTransport.Start(adapter.StartStateStart, serverScope)
if err != nil {
for _, startedTransport := range transports {
startedTransport.Close()
}
return nil, E.Cause(err, "initialize transport for ", serverAddr)
return nil, E.Errors(E.Cause(err, "initialize transport for ", serverAddr), serverScope.Close())
}
transports = append(transports, serverTransport)
}
newServerSet := &localServerSet{
config: systemConfig,
transports: transports,
config: systemConfig,
transports: transports,
serverScope: serverScope,
}
oldServerSet := t.serverSet.Swap(newServerSet)
if oldServerSet != nil {
oldServerSet.Close()
oldServerSet.serverScope.Close()
}
return newServerSet, nil
}
+17 -275
View File
@@ -2,129 +2,14 @@
package systemconfig
/*
#include <dlfcn.h>
#include <notify.h>
#include <stdint.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
// dnsinfo.h is not shipped in any SDK. The layouts below are DNSINFO_VERSION
// 20170629 from apple-oss-distributions/configd (#pragma pack(4)), the format
// libsystem_configuration unpacks into at runtime. dns_configuration_copy,
// dns_configuration_free and dns_configuration_notify_key are private
// libSystem exports. cgo silently drops packed struct fields that fall on
// unaligned offsets.
#pragma pack(4)
typedef struct {
struct in_addr address;
struct in_addr mask;
} box_dns_sortaddr_t;
typedef struct {
char *domain;
int32_t n_nameserver;
struct sockaddr **nameserver;
uint16_t port;
int32_t n_search;
char **search;
int32_t n_sortaddr;
box_dns_sortaddr_t **sortaddr;
char *options;
uint32_t timeout;
uint32_t search_order;
uint32_t if_index;
uint32_t flags;
uint32_t reach_flags;
uint32_t service_identifier;
char *cid;
char *if_name;
} box_dns_resolver_t;
typedef struct {
int32_t n_resolver;
box_dns_resolver_t **resolver;
int32_t n_scoped_resolver;
box_dns_resolver_t **scoped_resolver;
uint64_t generation;
int32_t n_service_specific_resolver;
box_dns_resolver_t **service_specific_resolver;
uint32_t version;
} box_dns_config_t;
#pragma pack()
static box_dns_config_t *(*box_dns_configuration_copy)(void);
static void (*box_dns_configuration_free)(box_dns_config_t *);
static void box_reverse_string(char *s) {
size_t length = strlen(s);
for (size_t i = 0; i < length / 2; i++) {
char tmp = s[i];
s[i] = s[length - 1 - i];
s[length - 1 - i] = tmp;
}
}
static int box_dnsinfo_load(void) {
if (box_dns_configuration_copy != NULL && box_dns_configuration_free != NULL) {
return 1;
}
char copy_name[] = "ypoc_noitarugifnoc_snd";
char free_name[] = "eerf_noitarugifnoc_snd";
box_reverse_string(copy_name);
box_reverse_string(free_name);
box_dns_configuration_copy = (box_dns_config_t * (*)(void)) dlsym(RTLD_DEFAULT, copy_name);
box_dns_configuration_free = (void (*)(box_dns_config_t *))dlsym(RTLD_DEFAULT, free_name);
return box_dns_configuration_copy != NULL && box_dns_configuration_free != NULL;
}
static box_dns_config_t *box_dnsinfo_copy(void) {
return box_dns_configuration_copy();
}
static void box_dnsinfo_free(box_dns_config_t *config) {
box_dns_configuration_free(config);
}
static const char *box_dnsinfo_notify_key(void) {
const char *(*notify_key)(void) = (const char *(*)(void))dlsym(RTLD_DEFAULT, "dns_configuration_notify_key");
if (notify_key != NULL) {
return notify_key();
}
return "com.apple.system.SystemConfiguration.dns_configuration";
}
static box_dns_resolver_t *box_dnsinfo_default_resolver(box_dns_config_t *config, int32_t index) {
return config->resolver[index];
}
static box_dns_resolver_t *box_dnsinfo_scoped_resolver(box_dns_config_t *config, int32_t index) {
return config->scoped_resolver[index];
}
static struct sockaddr *box_dnsinfo_nameserver(box_dns_resolver_t *resolver, int32_t index) {
return resolver->nameserver[index];
}
static const char *box_dnsinfo_search_domain(box_dns_resolver_t *resolver, int32_t index) {
return resolver->search[index];
}
*/
import "C"
import (
"context"
"encoding/binary"
"net/netip"
"strconv"
"sync"
"time"
"unsafe"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/dnsinfo"
"github.com/sagernet/sing/common"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
@@ -135,25 +20,15 @@ import (
type Source struct {
interfaceMonitor tun.DefaultInterfaceMonitor
access sync.Mutex
notifyToken C.int
notifyValid bool
stale bool
interfaceIndex int
config *Config
}
func NewSource(ctx context.Context) *Source {
source := &Source{
return &Source{
interfaceMonitor: service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor(),
}
if C.box_dnsinfo_load() != 0 {
var token C.int
if C.notify_register_check(C.box_dnsinfo_notify_key(), &token) == 0 {
source.notifyToken = token
source.notifyValid = true
}
}
return source
}
func (s *Source) Configuration() *Config {
@@ -162,19 +37,18 @@ func (s *Source) Configuration() *Config {
defer s.access.Unlock()
interfaceChanged := s.interfaceIndex != interfaceIndex
s.interfaceIndex = interfaceIndex
changed := s.changedLocked()
if s.config != nil && !s.stale && !interfaceChanged && !changed {
if s.config != nil && !s.stale && !interfaceChanged && s.interfaceMonitor != nil {
return s.config
}
s.stale = false
systemInfo := copyDNSInfo()
if systemInfo == nil {
dnsConfiguration := dnsinfo.Copy()
if dnsConfiguration == nil {
if s.config == nil {
s.config = new(dnsInfoConfig).build(interfaceIndex)
s.config = newConfig(dnsinfo.Resolver{})
}
return s.config
}
config := systemInfo.build(interfaceIndex)
config := newConfig(dnsConfiguration.Select(interfaceIndex))
if s.config != nil && config.Equal(s.config) {
return s.config
}
@@ -182,18 +56,6 @@ func (s *Source) Configuration() *Config {
return config
}
func (s *Source) changedLocked() bool {
if !s.notifyValid {
return true
}
var changed C.int
status := C.notify_check(s.notifyToken, &changed)
if status != 0 {
return true
}
return changed != 0
}
func (s *Source) Reset() {
s.access.Lock()
s.stale = true
@@ -201,12 +63,6 @@ func (s *Source) Reset() {
}
func (s *Source) Close() error {
s.access.Lock()
defer s.access.Unlock()
if s.notifyValid {
C.notify_cancel(s.notifyToken)
s.notifyValid = false
}
return nil
}
@@ -221,144 +77,30 @@ func (s *Source) defaultInterfaceIndex() int {
return defaultInterface.Index
}
type dnsInfoResolver struct {
interfaceIndex int
domain string
servers []M.Socksaddr
search []string
timeout time.Duration
}
type dnsInfoConfig struct {
resolvers []dnsInfoResolver
scopedResolvers []dnsInfoResolver
}
func (c *dnsInfoConfig) build(interfaceIndex int) *Config {
var selected dnsInfoResolver
if interfaceIndex != 0 {
selected = common.Find(c.scopedResolvers, func(it dnsInfoResolver) bool {
return it.interfaceIndex == interfaceIndex && len(it.servers) > 0
})
}
if len(selected.servers) == 0 {
selected = common.Find(c.resolvers, func(it dnsInfoResolver) bool {
return it.domain == "" && len(it.servers) > 0
})
}
func newConfig(resolver dnsinfo.Resolver) *Config {
config := &Config{
Ndots: 1,
Timeout: 5 * time.Second,
Attempts: 2,
}
if len(selected.servers) == 0 {
if len(resolver.Servers) == 0 {
config.Servers = defaultServers
config.Search = defaultSearch()
return config
}
config.Servers = selected.servers
if len(selected.search) > 0 {
config.Search = selected.search
} else {
config.Search = defaultSearch()
}
if selected.timeout > 0 {
config.Timeout = selected.timeout
}
return config
}
func copyDNSInfo() *dnsInfoConfig {
if C.box_dnsinfo_load() == 0 {
return nil
}
rawConfig := C.box_dnsinfo_copy()
if rawConfig == nil {
return nil
}
defer C.box_dnsinfo_free(rawConfig)
systemInfo := new(dnsInfoConfig)
for i := C.int32_t(0); i < rawConfig.n_resolver; i++ {
rawResolver := C.box_dnsinfo_default_resolver(rawConfig, i)
if rawResolver == nil {
continue
}
systemInfo.resolvers = append(systemInfo.resolvers, parseResolver(rawResolver))
}
for i := C.int32_t(0); i < rawConfig.n_scoped_resolver; i++ {
rawResolver := C.box_dnsinfo_scoped_resolver(rawConfig, i)
if rawResolver == nil {
continue
}
systemInfo.scopedResolvers = append(systemInfo.scopedResolvers, parseResolver(rawResolver))
}
return systemInfo
}
func parseResolver(rawResolver *C.box_dns_resolver_t) dnsInfoResolver {
resolver := dnsInfoResolver{
interfaceIndex: int(rawResolver.if_index),
domain: C.GoString(rawResolver.domain),
timeout: time.Duration(rawResolver.timeout) * time.Second,
}
interfaceName := C.GoString(rawResolver.if_name)
resolverPort := uint16(rawResolver.port)
if resolverPort == 0 {
resolverPort = 53
}
for i := C.int32_t(0); i < rawResolver.n_nameserver; i++ {
rawSockaddr := C.box_dnsinfo_nameserver(rawResolver, i)
if rawSockaddr == nil {
continue
}
serverAddr, loaded := parseSockaddr(rawSockaddr, resolverPort, interfaceName)
if !loaded {
continue
}
resolver.servers = append(resolver.servers, M.SocksaddrFromNetIP(serverAddr))
}
for i := C.int32_t(0); i < rawResolver.n_search; i++ {
searchDomain := C.GoString(C.box_dnsinfo_search_domain(rawResolver, i))
if searchDomain == "" {
continue
}
config.Servers = common.Map(resolver.Servers, M.SocksaddrFromNetIP)
for _, searchDomain := range resolver.Search {
searchDomain = mDNS.Fqdn(searchDomain)
if searchDomain == "." {
continue
}
resolver.search = append(resolver.search, searchDomain)
config.Search = append(config.Search, searchDomain)
}
return resolver
}
func parseSockaddr(rawSockaddr *C.struct_sockaddr, fallbackPort uint16, zone string) (netip.AddrPort, bool) {
switch rawSockaddr.sa_family {
case C.AF_INET:
sockaddrInet := (*C.struct_sockaddr_in)(unsafe.Pointer(rawSockaddr))
addr := netip.AddrFrom4(*(*[4]byte)(unsafe.Pointer(&sockaddrInet.sin_addr)))
return netip.AddrPortFrom(addr, sockaddrPort(unsafe.Pointer(&sockaddrInet.sin_port), fallbackPort)), true
case C.AF_INET6:
sockaddrInet6 := (*C.struct_sockaddr_in6)(unsafe.Pointer(rawSockaddr))
addr := netip.AddrFrom16(*(*[16]byte)(unsafe.Pointer(&sockaddrInet6.sin6_addr)))
if addr.IsLinkLocalUnicast() {
scopeId := uint32(sockaddrInet6.sin6_scope_id)
if zone == "" && scopeId != 0 {
zone = strconv.FormatUint(uint64(scopeId), 10)
}
if zone != "" {
addr = addr.WithZone(zone)
}
}
return netip.AddrPortFrom(addr, sockaddrPort(unsafe.Pointer(&sockaddrInet6.sin6_port), fallbackPort)), true
default:
return netip.AddrPort{}, false
if len(config.Search) == 0 {
config.Search = defaultSearch()
}
}
func sockaddrPort(rawPort unsafe.Pointer, fallbackPort uint16) uint16 {
port := binary.BigEndian.Uint16((*[2]byte)(rawPort)[:])
if port == 0 {
return fallbackPort
if resolver.Timeout > 0 {
config.Timeout = resolver.Timeout
}
return port
return config
}
+3 -7
View File
@@ -93,13 +93,9 @@ func NewRawTransport(transportAdapter dns.TransportAdapter, ctx context.Context,
}
}
func (t *Transport) Start(stage adapter.StartStage) error {
return nil
}
func (t *Transport) Close() error {
if t.configSource != nil {
return t.configSource.Close()
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage == adapter.StartStateInitialize && t.configSource != nil {
scope.Add(t.configSource.Close)
}
return nil
}
+22 -1
View File
@@ -47,6 +47,7 @@ type queryMultiplexer struct {
reuseState atomic.Int32
demoteFailures atomic.Int32
keepIdle atomic.Bool
probeAccess sync.Mutex
probeEpoch uint32
@@ -81,7 +82,7 @@ type pendingQuery struct {
}
func newQueryMultiplexer(options queryMultiplexerOptions) *queryMultiplexer {
return &queryMultiplexer{
multiplexer := &queryMultiplexer{
options: options,
queries: make(map[uint16]*pendingQuery),
connection: NewConnPool(ConnPoolOptions[*multiplexConn]{
@@ -94,6 +95,23 @@ func newQueryMultiplexer(options queryMultiplexerOptions) *queryMultiplexer {
},
}),
}
multiplexer.keepIdle.Store(true)
return multiplexer
}
func (m *queryMultiplexer) SetKeepIdleConnections(keep bool) {
m.keepIdle.Store(keep)
m.closeIdleConnection()
}
func (m *queryMultiplexer) CloseIdleConnections() {
m.connection.CloseIdle()
}
func (m *queryMultiplexer) closeIdleConnection() {
if !m.keepIdle.Load() {
m.connection.CloseIdle()
}
}
func (m *queryMultiplexer) Close() error {
@@ -307,6 +325,7 @@ func (m *queryMultiplexer) exchangeAsync(ctx context.Context, message *mDNS.Msg,
queryId, err := m.register(ctx, connCtx, conn, message, callback, retryReadError && m.options.retryReadError && !created)
if err != nil {
m.connection.Release(conn, true)
m.closeIdleConnection()
callback(nil, err)
return
}
@@ -408,6 +427,7 @@ func (m *queryMultiplexer) complete(queryId uint16, response *mDNS.Msg, err erro
response.Id = pending.message.Id
}
pending.callback(response, err)
m.closeIdleConnection()
}
func (m *queryMultiplexer) completeContextDone(queryId uint16, ctx context.Context) {
@@ -422,6 +442,7 @@ func (m *queryMultiplexer) completeContextDone(queryId uint16, ctx context.Conte
m.connection.Release(pending.conn, true)
}
pending.callback(nil, err)
m.closeIdleConnection()
}
func (m *queryMultiplexer) recvLoop(conn *multiplexConn) {
+12 -5
View File
@@ -9,9 +9,11 @@ import (
"testing"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
boxDNS "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
@@ -114,7 +116,16 @@ func newTestTCPTransport(t *testing.T, listener net.Listener) *TCPTransport {
if err != nil {
t.Fatal(err)
}
return NewTCPRaw(boxDNS.NewTransportAdapter(C.DNSTypeTCP, "test", nil), transportDialer, M.SocksaddrFromNet(listener.Addr()))
transport := NewTCPRaw(boxDNS.NewTransportAdapter(C.DNSTypeTCP, "test", nil), transportDialer, M.SocksaddrFromNet(listener.Addr()))
scope := adapter.NewScope(context.Background(), log.NewNOPFactory().Logger())
err = transport.Start(adapter.StartStateStart, scope)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
scope.Close()
})
return transport
}
func testExchange(transport *TCPTransport, questionName string) error {
@@ -155,7 +166,6 @@ func TestTCPTransportSingleQueryServer(t *testing.T) {
}()
transport := newTestTCPTransport(t, listener)
defer transport.Close()
const queryCount = 8
results := make(chan error, queryCount)
@@ -221,7 +231,6 @@ func TestTCPTransportProbeEnablesReuse(t *testing.T) {
}()
transport := newTestTCPTransport(t, listener)
defer transport.Close()
deadline := time.Now().Add(3 * time.Second)
for maxServedOnConn.Load() < 3 {
@@ -284,7 +293,6 @@ func TestTCPTransportDemotesBrokenReuse(t *testing.T) {
}()
transport := newTestTCPTransport(t, listener)
defer transport.Close()
deadline := time.Now().Add(3 * time.Second)
for {
@@ -364,7 +372,6 @@ func TestTCPTransportSilentPipelineServer(t *testing.T) {
}()
transport := newTestTCPTransport(t, listener)
defer transport.Close()
const queryCount = 5
results := make(chan error, queryCount)
+6 -7
View File
@@ -137,19 +137,18 @@ func (t *HTTP3Transport) newTransport() *http3.Transport {
}
}
func (t *HTTP3Transport) Start(stage adapter.StartStage) error {
func (t *HTTP3Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(func() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
return t.transport.Close()
})
return dialer.InitializeDetour(t.dialer)
}
func (t *HTTP3Transport) Close() error {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
return t.transport.Close()
}
func (t *HTTP3Transport) Reset() {
t.transportAccess.Lock()
defer t.transportAccess.Unlock()
+2 -5
View File
@@ -79,17 +79,14 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
func (t *Transport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(t.connection.Close)
return dialer.InitializeDetour(t.dialer)
}
func (t *Transport) Close() error {
return t.connection.Close()
}
func (t *Transport) Reset() {
t.connection.Reset()
}
+1 -5
View File
@@ -53,11 +53,7 @@ func (t *SDNSTransport) Name() string {
return t.name
}
func (t *SDNSTransport) Start(adapter.StartStage) error {
return nil
}
func (t *SDNSTransport) Close() error {
func (t *SDNSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
return nil
}
+14 -6
View File
@@ -22,7 +22,10 @@ import (
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*TCPTransport)(nil)
var (
_ adapter.DNSTransport = (*TCPTransport)(nil)
_ adapter.IdleConnectionKeeper = (*TCPTransport)(nil)
)
func RegisterTCP(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeTCP, NewTCP)
@@ -76,21 +79,26 @@ func NewTCPRaw(adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socks
return t
}
func (t *TCPTransport) Start(stage adapter.StartStage) error {
func (t *TCPTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(t.multiplexer.Close)
return dialer.InitializeDetour(t.dialer)
}
func (t *TCPTransport) Close() error {
return t.multiplexer.Close()
}
func (t *TCPTransport) Reset() {
t.multiplexer.Reset()
}
func (t *TCPTransport) SetKeepIdleConnections(keep bool) {
t.multiplexer.SetKeepIdleConnections(keep)
}
func (t *TCPTransport) CloseIdleConnections() {
t.multiplexer.CloseIdleConnections()
}
func (t *TCPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
return t.multiplexer.Exchange(ctx, message)
}
+14 -6
View File
@@ -20,7 +20,10 @@ import (
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*TLSTransport)(nil)
var (
_ adapter.DNSTransport = (*TLSTransport)(nil)
_ adapter.IdleConnectionKeeper = (*TLSTransport)(nil)
)
func RegisterTLS(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteTLSDNSServerOptions](registry, C.DNSTypeTLS, NewTLS)
@@ -82,21 +85,26 @@ func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
return t
}
func (t *TLSTransport) Start(stage adapter.StartStage) error {
func (t *TLSTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(t.multiplexer.Close)
return dialer.InitializeDetour(t.dialer)
}
func (t *TLSTransport) Close() error {
return t.multiplexer.Close()
}
func (t *TLSTransport) Reset() {
t.multiplexer.Reset()
}
func (t *TLSTransport) SetKeepIdleConnections(keep bool) {
t.multiplexer.SetKeepIdleConnections(keep)
}
func (t *TLSTransport) CloseIdleConnections() {
t.multiplexer.CloseIdleConnections()
}
func (t *TLSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
return t.multiplexer.Exchange(ctx, message)
}
+14 -6
View File
@@ -22,7 +22,10 @@ import (
mDNS "github.com/miekg/dns"
)
var _ adapter.DNSTransport = (*UDPTransport)(nil)
var (
_ adapter.DNSTransport = (*UDPTransport)(nil)
_ adapter.IdleConnectionKeeper = (*UDPTransport)(nil)
)
func RegisterUDP(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.RemoteDNSServerOptions](registry, C.DNSTypeUDP, NewUDP)
@@ -76,21 +79,26 @@ func NewUDPRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer
return t
}
func (t *UDPTransport) Start(stage adapter.StartStage) error {
func (t *UDPTransport) Start(stage adapter.StartStage, scope *adapter.Scope) error {
if stage != adapter.StartStateStart {
return nil
}
scope.Add(t.multiplexer.Close)
return dialer.InitializeDetour(t.dialer)
}
func (t *UDPTransport) Close() error {
return t.multiplexer.Close()
}
func (t *UDPTransport) Reset() {
t.multiplexer.Reset()
}
func (t *UDPTransport) SetKeepIdleConnections(keep bool) {
t.multiplexer.SetKeepIdleConnections(keep)
}
func (t *UDPTransport) CloseIdleConnections() {
t.multiplexer.CloseIdleConnections()
}
func (t *UDPTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
t.updateUDPSize(message)
response, err := t.multiplexer.Exchange(ctx, message)
+15
View File
@@ -8,6 +8,7 @@ type TransportAdapter struct {
transportType string
transportTag string
dependencies []string
references []string
}
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
@@ -23,10 +24,15 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
if localOptions.DomainResolver != nil && localOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, localOptions.DomainResolver.Server)
}
var references []string
if localOptions.Detour != "" {
references = []string{localOptions.Detour}
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
references: references,
}
}
@@ -35,10 +41,15 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
}
var references []string
if remoteOptions.Detour != "" {
references = []string{remoteOptions.Detour}
}
return TransportAdapter{
transportType: transportType,
transportTag: transportTag,
dependencies: dependencies,
references: references,
}
}
@@ -53,3 +64,7 @@ func (a *TransportAdapter) Tag() string {
func (a *TransportAdapter) Dependencies() []string {
return a.dependencies
}
func (a *TransportAdapter) References() []string {
return a.references
}
+22 -138
View File
@@ -2,46 +2,37 @@ package dns
import (
"context"
"io"
"os"
"strings"
"sync"
"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"
"github.com/sagernet/sing/common/logger"
)
var _ adapter.DNSTransportManager = (*TransportManager)(nil)
type TransportManager struct {
logger log.ContextLogger
registry adapter.DNSTransportRegistry
outbound adapter.OutboundManager
defaultTag string
access sync.RWMutex
started bool
stage adapter.StartStage
transports []adapter.DNSTransport
transportByTag map[string]adapter.DNSTransport
dependByTag map[string][]string
defaultTransport adapter.DNSTransport
defaultTransportFallback func() (adapter.DNSTransport, error)
fakeIPTransport adapter.FakeIPTransport
}
func NewTransportManager(logger logger.ContextLogger, registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager {
func NewTransportManager(registry adapter.DNSTransportRegistry, outbound adapter.OutboundManager, defaultTag string) *TransportManager {
return &TransportManager{
logger: logger,
registry: registry,
outbound: outbound,
defaultTag: defaultTag,
transportByTag: make(map[string]adapter.DNSTransport),
dependByTag: make(map[string][]string),
}
}
@@ -49,14 +40,9 @@ func (m *TransportManager) Initialize(defaultTransportFallback func() (adapter.D
m.defaultTransportFallback = defaultTransportFallback
}
func (m *TransportManager) Start(stage adapter.StartStage) error {
func (m *TransportManager) Start(stage adapter.StartStage, scope *adapter.Scope) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
if stage == adapter.StartStateStart {
if stage == adapter.StartStateInitialize {
if m.defaultTag != "" && m.defaultTransport == nil {
m.access.Unlock()
return E.New("default DNS server not found: ", m.defaultTag)
@@ -71,24 +57,23 @@ func (m *TransportManager) Start(stage adapter.StartStage) error {
m.transportByTag[defaultTransport.Tag()] = defaultTransport
m.defaultTransport = defaultTransport
}
transports := m.transports
m.access.Unlock()
return m.startTransports(transports)
} else {
transports := m.transports
m.access.Unlock()
for _, outbound := range transports {
err := adapter.LegacyStart(outbound, stage)
if err != nil {
return E.Cause(err, stage, " dns/", outbound.Type(), "[", outbound.Tag(), "]")
}
}
transports := m.transports
m.access.Unlock()
if stage == adapter.StartStateStart {
return m.startTransports(scope, transports)
}
for _, transport := range transports {
name := "dns/" + transport.Type() + "[" + transport.Tag() + "]"
err := scope.Start(name, transport, stage)
if err != nil {
return err
}
}
return nil
}
func (m *TransportManager) startTransports(transports []adapter.DNSTransport) error {
monitor := taskmonitor.New(m.logger, C.StartTimeout)
func (m *TransportManager) startTransports(scope *adapter.Scope, transports []adapter.DNSTransport) error {
started := make(map[string]bool)
for {
canContinue := false
@@ -106,13 +91,10 @@ func (m *TransportManager) startTransports(transports []adapter.DNSTransport) er
}
started[transportTag] = true
canContinue = true
if starter, isStarter := transportToStart.(adapter.Lifecycle); isStarter {
monitor.Start("start dns/", transportToStart.Type(), "[", transportTag, "]")
err := starter.Start(adapter.StartStateStart)
monitor.Finish()
if err != nil {
return E.Cause(err, "start dns/", transportToStart.Type(), "[", transportTag, "]")
}
name := "dns/" + transportToStart.Type() + "[" + transportTag + "]"
err := scope.Start(name, transportToStart, adapter.StartStateStart)
if err != nil {
return err
}
}
if len(started) == len(transports) {
@@ -145,30 +127,6 @@ func (m *TransportManager) startTransports(transports []adapter.DNSTransport) er
return nil
}
func (m *TransportManager) Close() error {
monitor := taskmonitor.New(m.logger, C.StopTimeout)
m.access.Lock()
if !m.started {
m.access.Unlock()
return nil
}
m.started = false
transports := m.transports
m.transports = nil
m.access.Unlock()
var err error
for _, transport := range transports {
if closer, isCloser := transport.(io.Closer); isCloser {
monitor.Start("close server/", transport.Type(), "[", transport.Tag(), "]")
err = E.Append(err, closer.Close(), func(err error) error {
return E.Cause(err, "close server/", transport.Type(), "[", transport.Tag(), "]")
})
monitor.Finish()
}
}
return nil
}
func (m *TransportManager) Transports() []adapter.DNSTransport {
m.access.RLock()
defer m.access.RUnlock()
@@ -194,54 +152,6 @@ func (m *TransportManager) FakeIP() adapter.FakeIPTransport {
return m.fakeIPTransport
}
func (m *TransportManager) Remove(tag string) error {
m.access.Lock()
defer m.access.Unlock()
transport, found := m.transportByTag[tag]
if !found {
return os.ErrInvalid
}
delete(m.transportByTag, tag)
index := common.Index(m.transports, func(it adapter.DNSTransport) bool {
return it == transport
})
if index == -1 {
panic("invalid inbound index")
}
m.transports = append(m.transports[:index], m.transports[index+1:]...)
started := m.started
if m.defaultTransport == transport {
if len(m.transports) > 0 {
nextTransport := m.transports[0]
if nextTransport.Type() != C.DNSTypeFakeIP {
return E.New("default server cannot be fakeip")
}
m.defaultTransport = nextTransport
m.logger.Notice("updated default server to ", m.defaultTransport.Tag())
} else {
m.defaultTransport = nil
}
}
dependBy := m.dependByTag[tag]
if len(dependBy) > 0 {
return E.New("server[", tag, "] is depended by ", strings.Join(dependBy, ", "))
}
dependencies := transport.Dependencies()
for _, dependency := range dependencies {
if len(m.dependByTag[dependency]) == 1 {
delete(m.dependByTag, dependency)
} else {
m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool {
return it != tag
})
}
}
if started {
transport.Close()
}
return nil
}
func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) error {
if tag == "" {
return os.ErrInvalid
@@ -252,43 +162,17 @@ func (m *TransportManager) Create(ctx context.Context, logger log.ContextLogger,
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(transport, stage)
if err != nil {
return E.Cause(err, stage, " dns/", transport.Type(), "[", transport.Tag(), "]")
}
}
}
if existsTransport, loaded := m.transportByTag[tag]; loaded {
if m.started {
err = common.Close(existsTransport)
if err != nil {
return E.Cause(err, "close dns/", existsTransport.Type(), "[", existsTransport.Tag(), "]")
}
}
existsIndex := common.Index(m.transports, func(it adapter.DNSTransport) bool {
return it == existsTransport
})
if existsIndex == -1 {
panic("invalid inbound index")
}
m.transports = append(m.transports[:existsIndex], m.transports[existsIndex+1:]...)
_, loaded := m.transportByTag[tag]
if loaded {
return E.New("duplicate DNS server tag: ", tag)
}
m.transports = append(m.transports, transport)
m.transportByTag[tag] = transport
dependencies := transport.Dependencies()
for _, dependency := range dependencies {
m.dependByTag[dependency] = append(m.dependByTag[dependency], tag)
}
if tag == m.defaultTag || (m.defaultTag == "" && m.defaultTransport == nil) {
if transport.Type() == C.DNSTypeFakeIP {
return E.New("default server cannot be fakeip")
}
m.defaultTransport = transport
if m.started {
m.logger.Notice("updated default server to ", transport.Tag())
}
}
if transport.Type() == C.DNSTypeFakeIP {
if m.fakeIPTransport != nil {
+40 -64
View File
@@ -2,6 +2,14 @@
icon: material/alert-decagram
---
#### 1.14.3
* Fixes and improvements
#### 1.14.2
* Fixes and improvements
#### 1.14.1
* Fixes and improvements
@@ -181,14 +189,11 @@ supplied at runtime through the API service by the graphical clients or the
**10**:
The new [Hysteria Realm service](/configuration/service/hysteria-realm/)
is a rendezvous service for Hysteria2 NAT traversal. A Hysteria2 server
behind NAT registers its STUN-discovered public addresses on a stable
realm endpoint via the new
[`realm`](/configuration/inbound/hysteria2/#realm) inbound field;
clients query the realm via the new
[`realm`](/configuration/outbound/hysteria2/#realm) outbound field to
learn the server's current addresses and perform UDP hole-punching to
establish a direct QUIC connection.
is a rendezvous service for Hysteria2 NAT traversal. Hysteria2 servers behind
NAT register on the realm via the new
[`realm`](/configuration/inbound/hysteria2/#realm) inbound field, and clients
connect to them through the realm via the new
[`realm`](/configuration/outbound/hysteria2/#realm) outbound field.
[`realm.ip_version`](/configuration/outbound/hysteria2/#realmip_version)
restricts realm connections to a single IP version, and
[`realm.port_mapping`](/configuration/outbound/hysteria2/#realmport_mapping)
@@ -243,8 +248,7 @@ See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
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
`server` set). Combining these fields with any of the legacy DNS fields deprecated
in **12** 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).
@@ -274,8 +278,7 @@ file option. A per-query
field is also available on DNS rule actions and the `resolve` route rule
action.
This deprecates the `independent_cache` DNS option (the DNS cache now
always keys by transport) and the `store_rdrc` cache file option
This deprecates the `independent_cache` DNS option and the `store_rdrc` cache file option
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
See [Migration](/migration/#migrate-independent-dns-cache).
@@ -291,9 +294,8 @@ and a `timeout` field on
The new [mDNS DNS server](/configuration/dns/server/mdns/) sends queries via
multicast on the local network. The default
[local DNS server](/configuration/dns/server/local/) also routes queries for
`*.local.` and IPv4/IPv6 link-local reverse zones via mDNS on non-Apple
platforms (and via the system resolver on Apple), and the new
[local DNS server](/configuration/dns/server/local/) also resolves
`*.local.` and IPv4/IPv6 link-local reverse zones, and the new
[`neighbor_domain`](/configuration/dns/server/local/#neighbor_domain) option
answers single-label hosts from the
[neighbor resolver](/configuration/shared/neighbor/).
@@ -327,8 +329,7 @@ The default `hijack` mode now sets the platform's native interface DNS
(`systemd-resolved` on Linux, per-interface DNS on Windows and Apple) and
installs platform-level DNS hijacking (an `iproute2` rule on Linux,
nftables DNAT when `auto_redirect` is enabled, WFP filters on Windows when
`strict_route` is enabled). Earlier versions did not touch the interface
DNS or the platform firewall.
`strict_route` is enabled).
**20**:
@@ -341,9 +342,7 @@ TUN and TProxy inbounds and the WireGuard endpoint.
**21**:
For UDP connections, the first packet is available in pre-match, so protocol
sniffing runs on it directly and rule matching continues with the sniffed
metadata.
The `sniff` action now works in pre-match for UDP connections.
See [Pre-match](/configuration/shared/pre-match/#sniff).
@@ -360,8 +359,8 @@ previously inlined in each component.
[`route.default_http_client`](/configuration/route/#default_http_client)
selects the default client for remote rule-sets. The legacy fallback
(use the default outbound when `http_clients` is empty altogether) is
preserved with a deprecation warning and will be removed in sing-box
1.16.0, together with the legacy `download_detour` remote rule-set option.
deprecated and will be removed in sing-box 1.16.0, together with the legacy
`download_detour` remote rule-set option.
**23**:
@@ -415,7 +414,7 @@ through Schannel via SSPI on Windows build 17763 or later.
The new `apple` value for outbound TLS `engine` routes the TLS handshake
through `Network.framework`, and the new `apple`
[HTTP client `engine`](/configuration/shared/http-client/#engine) routes HTTP
requests through `NSURLSession`. The default remains `go`.
requests through `NSURLSession`.
**28**:
@@ -444,8 +443,7 @@ The rule-set [`tag`](/configuration/rule-set/#tag) field now accepts a list of
tags to define multiple rule-sets sharing other options at once, with the
`{tag}` placeholder in `path` or `url` replaced by each tag. The new
[`initial_path`](/configuration/rule-set/#initial_path) option provides
initial content for remote rule-sets so startup is not blocked by the initial
download.
initial content for remote rule-sets.
**31**:
@@ -692,9 +690,7 @@ split-DNS resolvers and, when enabled, general pushed resolvers.
The [OpenConnect Client](/configuration/endpoint/openconnect/) endpoint can now
submit Fortinet host check results using the new
[`fortinet_host_check`](/configuration/endpoint/openconnect/#fortinet_host_check)
option. This behavior is modeled after openfortivpn and is not an OpenConnect
feature. sing-box only submits explicitly configured values when requested by
the Fortinet server and does not collect system information automatically.
option.
#### 1.14.0-alpha.48
@@ -1036,15 +1032,11 @@ The `control_http_client` field on
**1**:
The new [Hysteria Realm service](/configuration/service/hysteria-realm/)
is a rendezvous service for Hysteria2 NAT traversal. A Hysteria2 server
behind NAT registers its STUN-discovered public addresses on a stable
realm endpoint via the new
[`realm`](/configuration/inbound/hysteria2/#realm) inbound field;
clients query the realm via the new
[`realm`](/configuration/outbound/hysteria2/#realm) outbound field to
learn the server's current addresses and perform UDP hole-punching to
establish a direct QUIC connection. Once hole-punching succeeds, all
proxy traffic flows directly between client and server.
is a rendezvous service for Hysteria2 NAT traversal. Hysteria2 servers behind
NAT register on the realm via the new
[`realm`](/configuration/inbound/hysteria2/#realm) inbound field, and clients
connect to them through the realm via the new
[`realm`](/configuration/outbound/hysteria2/#realm) outbound field.
#### 1.14.0-alpha.21
@@ -1064,19 +1056,14 @@ The default `hijack` mode now sets the platform's native interface DNS
(`systemd-resolved` on Linux, per-interface DNS on Windows and Apple) and
installs platform-level DNS hijacking (an `iproute2` rule on Linux,
nftables DNAT when `auto_redirect` is enabled, WFP filters on Windows when
`strict_route` is enabled). Earlier versions did not touch the interface
DNS or the platform firewall.
`strict_route` is enabled).
**2**:
The new [mDNS DNS server](/configuration/dns/server/mdns/) sends queries via
multicast on the local network. The default
[local DNS server](/configuration/dns/server/local/) also routes queries for
`*.local.` and IPv4/IPv6 link-local reverse zones via mDNS on non-Apple
platforms (and via the system resolver on Apple), so an explicit `mdns`
server is only needed to reference it from
[`preferred_by`](/configuration/dns/rule/#preferred_by) or to use it
standalone.
[local DNS server](/configuration/dns/server/local/) also resolves
`*.local.` and IPv4/IPv6 link-local reverse zones.
**3**:
@@ -1189,20 +1176,13 @@ HTTP/2 and QUIC parameters). Components that make outbound HTTP requests
DERP `verify_client_url`, and the Tailscale `control_http_client` — now
accept an inline HTTP client object or the tag of an `http_clients`
entry, replacing the dial and TLS fields previously inlined in each
component. When the field is omitted, ACME, Cloudflare Origin CA, DERP
and Tailscale dial direct (their existing default).
component.
Remote rule-sets are the only HTTP-using component whose default for an
omitted `http_client` has historically resolved to the default outbound,
not to direct, and a typical configuration contains many of them. To
avoid repeating the same `http_client` block in every rule-set,
[`route.default_http_client`](/configuration/route/#default_http_client)
selects a default rule-set client by tag and is the only field that
consults it. If `default_http_client` is empty and `http_clients` is
non-empty, the first entry is used automatically. The legacy fallback
(use the default outbound when `http_clients` is empty altogether) is
preserved with a deprecation warning and will be removed in sing-box
1.16.0, together with the legacy `download_detour` remote rule-set
selects the default HTTP client for remote rule-sets; if empty, the first
`http_clients` entry is used. The legacy fallback (use the default outbound
when `http_clients` is empty altogether) is deprecated and will be removed in
sing-box 1.16.0, together with the legacy `download_detour` remote rule-set
option and the legacy dialer fields on Tailscale endpoints.
**2**:
@@ -1216,9 +1196,7 @@ places:
the TLS handshake through `Network.framework` for direct TCP TLS
client connections.
The default remains `go`. Both engines come with additional CGO and
framework memory overhead and platform restrictions documented on each
field.
Both engines have platform restrictions documented on each field.
**3**:
@@ -1276,8 +1254,7 @@ file option. A per-query
field is also available on DNS rule actions and the `resolve` route rule
action.
This deprecates the `independent_cache` DNS option (the DNS cache now
always keys by transport) and the `store_rdrc` cache file option
This deprecates the `independent_cache` DNS option and the `store_rdrc` cache file option
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
See [Migration](/migration/#migrate-independent-dns-cache).
@@ -1316,8 +1293,7 @@ See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
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
`server` set). 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).
+5 -7
View File
@@ -7,10 +7,6 @@ icon: material/apple
SFI/SFM/SFT allows users to manage and run local or remote sing-box configuration files, and provides
platform-specific function implementation, such as TUN transparent proxy implementation.
!!! failure ""
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
## :material-graph: Requirements
* iOS 15.0+ / macOS 13.0+ / Apple tvOS 17.0+
@@ -18,7 +14,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
* [App Store](https://apps.apple.com/app/sing-box-mt/id6785326793) (iOS / Apple tvOS)
* TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
@@ -28,9 +24,11 @@ or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## :material-cellphone-arrow-down: Download (iOS jailbreak version)
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) (`SFI-iphoneos-arm64.deb`)
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
* `SFI-iphoneos-arm64.deb`: rootless
* `SFI-iphoneos-arm64e.deb`: roothide
The jailbroken version of SFI: requires rootless iOS 15.0+
The jailbroken version of SFI: requires rootless or roothide iOS 15.0+
Additional features:
+1 -1
View File
@@ -2,7 +2,7 @@
#### Service
SFW runs sing-box as a system service, so no administrator elevation is required for daily use.
SFW runs sing-box as a system service.
### Chore
+1 -1
View File
@@ -77,7 +77,7 @@ Conflict with `optimistic`.
!!! 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).
`independent_cache` is deprecated and will be removed in sing-box 1.16.0, check [Migration](/migration/#migrate-independent-dns-cache).
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
+4 -8
View File
@@ -262,10 +262,8 @@ Tags of [Inbound](/configuration/inbound/).
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.
`server` set. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
@@ -285,10 +283,8 @@ Not limited if empty.
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.
`server` set. See
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
Setting this field makes the DNS rule incompatible in the same DNS
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
+2 -4
View File
@@ -260,8 +260,7 @@ icon: material/alert-decagram
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
@@ -279,8 +278,7 @@ icon: material/alert-decagram
!!! quote "sing-box 1.14.0 中的更改"
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。参阅
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
+4 -8
View File
@@ -55,7 +55,7 @@ immediately; the remaining queries are canceled.
Rules without `race` still take effect strictly in listed order: while a preceding race rule is
not yet judged, the action of any other matched rule is held until none of the race rules
matched. The result may therefore depend on server speed only among race rules.
matched.
### route
@@ -92,8 +92,7 @@ By default, no query is sent in parallel with pending race rules: a matched `rou
holds its query until none of the race rules matched.
When `speculative` is enabled, the query is sent as soon as the rule matches, in parallel with
the pending race rules, and may be wasted: its response is still used only after none of the
race rules matched.
the pending race rules; its response is used only after none of the race rules matched.
#### strategy
@@ -170,8 +169,7 @@ 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.
require a preceding top-level rule with `evaluate` action.
#### server
@@ -196,7 +194,7 @@ By default, no query is sent in parallel with pending race rules: a matched `eva
holds its query, and rule matching stops there, until none of the race rules matched.
When `speculative` is enabled, the query is sent as soon as the rule matches, in parallel with
the pending race rules, and may be wasted: rule matching continues without waiting for them.
the pending race rules, and rule matching continues without waiting for them.
#### disable_cache
@@ -248,8 +246,6 @@ Conflict with `client_subnet`.
`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.
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
+4 -7
View File
@@ -48,7 +48,7 @@ icon: material/new-box
启用 `race` 的规则成为竞态规则,不再保持这一顺序:其引用的响应尚未到达时,规则匹配会越过它继续进行,因此竞态规则的匹配相互并行、也与后续规则并行。每条竞态规则在其引用的响应可用时被判定,首个匹配的竞态规则立即终止规则评估,其余查询将被取消。
未启用 `race` 的规则仍严格按顺序生效:只要前面还有未判定的竞态规则,其他已匹配规则的动作就被扣住,直到所有竞态规则均未匹配。因此只有竞态规则之间的结果取决于服务器速度。
未启用 `race` 的规则仍严格按顺序生效:只要前面还有未判定的竞态规则,其他已匹配规则的动作就被扣住,直到所有竞态规则均未匹配。
### route
@@ -83,7 +83,7 @@ icon: material/new-box
默认情况下,查询决不与未判定的竞态规则并行发出:已匹配的 `route` 动作扣住其查询,直到所有竞态规则均未匹配后才发送。
启用 `speculative` 后,查询成为投机查询:在规则匹配时立即发出、与未判定的竞态规则并行,且可能被浪费;其响应仍仅在所有竞态规则均未匹配后才被使用。
启用 `speculative` 后,查询在规则匹配时立即发出、与未判定的竞态规则并行;其响应仅在所有竞态规则均未匹配后才被使用。
#### strategy
@@ -158,8 +158,7 @@ icon: material/new-box
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
因为匹配发生在动作执行之前。
需要位于更早的顶层 `evaluate` 规则之后。
#### server
@@ -182,7 +181,7 @@ icon: material/new-box
默认情况下,查询决不与未判定的竞态规则并行发出:已匹配的 `evaluate` 动作扣住其查询,规则匹配在此处停止,直到所有竞态规则均未匹配。
启用 `speculative` 后,查询成为投机查询:在规则匹配时立即发出、与未判定的竞态规则并行,且可能被浪费;规则匹配继续进行而不等待竞态规则。
启用 `speculative` 后,查询在规则匹配时立即发出、与未判定的竞态规则并行,规则匹配继续进行而不等待竞态规则。
#### disable_cache
@@ -234,8 +233,6 @@ icon: material/new-box
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
此动作不会发起新的 DNS 查询。
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
### route-options
+1 -1
View File
@@ -27,7 +27,7 @@ icon: material/new-box
!!! info ""
You usually do not need an explicit `mdns` server in addition to a [Local](./local/) server: the local server already routes queries for `*.local.` and IPv4/IPv6 link-local reverse zones via mDNS on non-Apple platforms and via the system resolver on Apple platforms. Add an explicit `mdns` server only when you want to reference it from [`preferred_by`](../rule/#preferred_by) or use it standalone.
`*.local.` and IPv4/IPv6 link-local reverse zones are also resolved by the [Local](./local/) server.
### Fields
+1 -1
View File
@@ -27,7 +27,7 @@ icon: material/new-box
!!! info ""
通常不需要在 [Local](./local/) 服务器之外再添加显式的 `mdns` 服务器:本地服务器已经会在非 Apple 平台通过 mDNS、在 Apple 平台通过系统解析器来回答 `*.local.` 与 IPv4/IPv6 链路本地反向区域的查询。仅当需要从 [`preferred_by`](../rule/#preferred_by) 引用,或独立使用时,才需要显式添加 `mdns` 服务器。
[Local](./local/) 服务器也会解析 `*.local.` 与 IPv4/IPv6 链路本地反向区域。
### 字段
+1 -3
View File
@@ -33,9 +33,7 @@ icon: material/new-box
The tag of the [OpenConnect Endpoint](/configuration/endpoint/openconnect).
DNS queries are sent to the resolvers pushed by the VPN server through the OpenConnect endpoint. Pushed split-DNS rules use their dedicated resolvers, while pushed split-DNS and search-domain suffixes use the general pushed resolvers. The most specific matching suffix takes precedence.
Pushed DNS settings are not installed into the operating system.
DNS queries are sent to the resolvers pushed by the VPN server through the OpenConnect endpoint.
#### accept_default_resolvers
@@ -33,9 +33,7 @@ icon: material/new-box
[OpenConnect 端点](/zh/configuration/endpoint/openconnect) 的标签。
DNS 查询会通过 OpenConnect 端点发送到 VPN 服务器推送的解析器。推送的分流 DNS 规则使用各自的专用解析器,推送的分流 DNS 和搜索域后缀则使用通用推送解析器。匹配时优先使用最具体的后缀。
推送的 DNS 设置不会安装到操作系统中。
DNS 查询会通过 OpenConnect 端点发送到 VPN 服务器推送的解析器。
#### accept_default_resolvers
+2 -4
View File
@@ -33,11 +33,9 @@ icon: material/new-box
The tag of the [OpenVPN Client Endpoint](/configuration/endpoint/openvpn-client).
DNS queries are sent through the endpoint to resolvers pushed by the OpenVPN server. Modern OpenVPN `dns server` options support plain DNS, DNS over TLS, DNS over HTTPS, custom ports, SNI, and `resolve-domains`. Only the server group with the lowest priority number is active. Legacy `dhcp-option DNS`/`DNS6` and `DOMAIN-ROUTE` are used when no modern server group is present.
DNS queries are sent through the endpoint to resolvers pushed by the OpenVPN server.
A modern server group overrides legacy DHCP DNS resolver and domain options. A standalone modern `dns search-domains` option does not remove legacy resolvers. Required DNSSEC validation (`dnssec yes`) is rejected because this transport does not provide DNSSEC validation.
Pushed DNS settings are not installed into the operating system.
Both modern `dns` options and legacy `dhcp-option DNS`/`DNS6` and `DOMAIN-ROUTE` options are supported. `dnssec yes` is not supported.
#### accept_default_resolvers
+2 -4
View File
@@ -33,11 +33,9 @@ icon: material/new-box
[OpenVPN 客户端端点](/zh/configuration/endpoint/openvpn-client) 的标签。
DNS 查询会通过该端点发送到 OpenVPN 服务器推送的解析器。现代 OpenVPN `dns server` 选项支持普通 DNS、DNS over TLS、DNS over HTTPS、自定义端口、SNI 和 `resolve-domains`。只有优先级数字最低的服务器组会生效。没有现代服务器组时,使用传统的 `dhcp-option DNS`/`DNS6` 和 `DOMAIN-ROUTE`。
DNS 查询会通过该端点发送到 OpenVPN 服务器推送的解析器。
现代服务器组会覆盖传统 DHCP DNS 解析器及相关域选项。只有现代 `dns search-domains` 而没有现代服务器组时,不会移除传统解析器。由于此传输不提供 DNSSEC 验证,需要强制验证的 `dnssec yes` 会被拒绝。
推送的 DNS 设置不会安装到操作系统中。
支持现代 `dns` 选项,以及传统的 `dhcp-option DNS`/`DNS6` 和 `DOMAIN-ROUTE` 选项。不支持 `dnssec yes`。
#### accept_default_resolvers
@@ -49,8 +49,6 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
When enabled, single-label queries (e.g. `my-device`) are retried against each Tailscale search domain until one resolves.
Default resolvers are not consulted for single-label queries regardless of `accept_default_resolvers`.
### Examples
=== "MagicDNS only"

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