mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-20 00:04:24 +00:00
Merge tag 'v1.14.3'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ./... && \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
Outbounds() []Outbound
|
||||
|
||||
+40
-72
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -22,3 +22,8 @@ type V2RayClientTransport interface {
|
||||
DialContext(ctx context.Context) (net.Conn, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type V2RayMultiplexClientTransport interface {
|
||||
V2RayClientTransport
|
||||
MultiplexEnabled() bool
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
Submodule clients/desktop updated: fb1175ca3b...50f5038491
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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:]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -347,5 +347,5 @@ func (c *realityClientConnWrapper) ReaderReplaceable() bool {
|
||||
}
|
||||
|
||||
func (c *realityClientConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -254,5 +254,5 @@ func (c *realityConnWrapper) ReaderReplaceable() bool {
|
||||
}
|
||||
|
||||
func (c *realityConnWrapper) WriterReplaceable() bool {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package urltest
|
||||
|
||||
import "context"
|
||||
|
||||
func contextWithQUICKeepSession(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build !(ios && tvos)
|
||||
|
||||
package constant
|
||||
|
||||
const IsTvOS = false
|
||||
@@ -0,0 +1,5 @@
|
||||
//go:build ios && tvos
|
||||
|
||||
package constant
|
||||
|
||||
const IsTvOS = true
|
||||
+29
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 := ¶llelExchange{
|
||||
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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 配置中不能与
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 链路本地反向区域。
|
||||
|
||||
### 字段
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user