Compare commits

...

22 Commits

Author SHA1 Message Date
renovate[bot]
f0d7533fa3
[dependencies] Update golangci/golangci-lint-action action to v8 2025-06-08 13:24:16 +00:00
世界
8413d1294d
documentation: Bump version 2025-06-08 21:22:58 +08:00
世界
c6e91daec7
release: Update Go to 1.24.4 2025-06-08 21:22:58 +08:00
世界
18dc7bbced
Fix tproxy listener 2025-06-08 21:22:18 +08:00
世界
5fc634a55b
Fix systemd package 2025-06-08 21:22:18 +08:00
世界
5f6aba9426
Fix missing home for derp service 2025-06-08 21:22:18 +08:00
Zero Clover
6e6e5027bb
documentation: Fix services 2025-06-08 21:22:18 +08:00
世界
2b284b746a
Fix dns.client_subnet ignored 2025-06-08 21:22:18 +08:00
世界
a8b25bf537
documentation: Minor fixes 2025-06-08 21:22:18 +08:00
世界
50e947ea49
Fix tailscale forward 2025-06-08 21:22:18 +08:00
世界
cfa9125eed
Minor fixes 2025-06-08 21:22:18 +08:00
世界
4c1b8909d8
Add SSM API service 2025-06-08 21:22:18 +08:00
世界
c06fe358e2
Add resolved service and DNS server 2025-06-08 21:22:18 +08:00
世界
4b90ada477
Add DERP service 2025-06-08 21:22:18 +08:00
世界
a8a7cb2b4d
Add service component type 2025-06-08 21:22:18 +08:00
世界
05390b9e91
Fix tproxy tcp control 2025-06-08 21:22:18 +08:00
愚者
30cd4c614f
release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-06-08 21:22:18 +08:00
世界
bd54f5e24c
prevent creation of bind and mark controls on unsupported platforms 2025-06-08 21:22:18 +08:00
PuerNya
f955fa0bd9
documentation: Fix description of reject DNS action behavior 2025-06-08 21:22:18 +08:00
Restia-Ashbell
21a9ef21e7
Fix TLS record fragment 2025-06-08 21:22:18 +08:00
世界
e3f689612c
Add missing accept_routes option for Tailscale 2025-06-08 21:22:18 +08:00
世界
4873f6f66b
Add TLS record fragment support 2025-06-08 21:22:18 +08:00
99 changed files with 4007 additions and 222 deletions

View File

@ -8,11 +8,15 @@
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues" --deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes --no-deb-generate-changes
--config-files /etc/sing-box/config.json --config-files /etc/sing-box/config.json
--after-install release/config/sing-box.postinst
release/config/config.json=/etc/sing-box/config.json release/config/config.json=/etc/sing-box/config.json
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

View File

@ -46,7 +46,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@ -109,7 +109,7 @@ jobs:
if: ${{ ! matrix.legacy_go }} if: ${{ ! matrix.legacy_go }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Cache Legacy Go - name: Cache Legacy Go
if: matrix.require_legacy_go if: matrix.require_legacy_go
id: cache-legacy-go id: cache-legacy-go
@ -294,7 +294,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@ -374,7 +374,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@ -472,7 +472,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Setup Xcode stable - name: Setup Xcode stable
if: matrix.if && github.ref == 'refs/heads/main-next' if: matrix.if && github.ref == 'refs/heads/main-next'
run: |- run: |-

View File

@ -28,9 +28,9 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@v8
with: with:
version: latest version: latest
args: --timeout=30m args: --timeout=30m

View File

@ -25,7 +25,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@ -66,7 +66,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.24 go-version: ^1.24.4
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@ -80,7 +80,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |

View File

@ -55,6 +55,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@ -136,6 +136,12 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/sing-box@.service - src: release/config/sing-box@.service
dst: /usr/lib/systemd/system/sing-box@.service dst: /usr/lib/systemd/system/sing-box@.service
- src: release/config/sing-box.sysusers
dst: /usr/lib/sysusers.d/sing-box.conf
- src: release/config/sing-box.rules
dst: /usr/share/polkit-1/rules.d/sing-box.rules
- src: release/config/sing-box-split-dns.xml
dst: /usr/share/dbus-1/system.d/sing-box-split-dns.conf
- src: release/completions/sing-box.bash - src: release/completions/sing-box.bash
dst: /usr/share/bash-completion/completions/sing-box.bash dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@ -1,11 +1,10 @@
NAME = sing-box NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD) COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_dhcp,with_wireguard,with_clash_api,with_quic,with_utls,with_tailscale TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_utls
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag) VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid=" PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)" MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"

View File

@ -7,7 +7,9 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@ -31,11 +33,30 @@ type DNSClient interface {
} }
type DNSQueryOptions struct { type DNSQueryOptions struct {
Transport DNSTransport Transport DNSTransport
Strategy C.DomainStrategy Strategy C.DomainStrategy
DisableCache bool LookupStrategy C.DomainStrategy
RewriteTTL *uint32 DisableCache bool
ClientSubnet netip.Prefix RewriteTTL *uint32
ClientSubnet netip.Prefix
}
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
if options == nil {
return &DNSQueryOptions{}, nil
}
transportManager := service.FromContext[DNSTransportManager](ctx)
transport, loaded := transportManager.Transport(options.Server)
if !loaded {
return nil, E.New("domain resolver not found: " + options.Server)
}
return &DNSQueryOptions{
Transport: transport,
Strategy: C.DomainStrategy(options.Strategy),
DisableCache: options.DisableCache,
RewriteTTL: options.RewriteTTL,
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
}, nil
} }
type RDRCStore interface { type RDRCStore interface {

View File

@ -7,7 +7,7 @@ import (
) )
type FakeIPStore interface { type FakeIPStore interface {
Service SimpleLifecycle
Contains(address netip.Addr) bool Contains(address netip.Addr) bool
Create(domain string, isIPv6 bool) (netip.Addr, error) Create(domain string, isIPv6 bool) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool) Lookup(address netip.Addr) (string, bool)

View File

@ -74,6 +74,7 @@ type InboundContext struct {
UDPTimeout time.Duration UDPTimeout time.Duration
TLSFragment bool TLSFragment bool
TLSFragmentFallbackDelay time.Duration TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
NetworkStrategy *C.NetworkStrategy NetworkStrategy *C.NetworkStrategy
NetworkType []C.InterfaceType NetworkType []C.InterfaceType

View File

@ -37,13 +37,14 @@ func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endp
func (m *Manager) Start(stage adapter.StartStage) error { func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock() m.access.Lock()
defer m.access.Unlock()
if m.started && m.stage >= stage { if m.started && m.stage >= stage {
panic("already started") panic("already started")
} }
m.started = true m.started = true
m.stage = stage m.stage = stage
for _, inbound := range m.inbounds { inbounds := m.inbounds
m.access.Unlock()
for _, inbound := range inbounds {
err := adapter.LegacyStart(inbound, stage) err := adapter.LegacyStart(inbound, stage)
if err != nil { if err != nil {
return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]")

View File

@ -2,6 +2,11 @@ package adapter
import E "github.com/sagernet/sing/common/exceptions" import E "github.com/sagernet/sing/common/exceptions"
type SimpleLifecycle interface {
Start() error
Close() error
}
type StartStage uint8 type StartStage uint8
const ( const (

View File

@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
} }
type lifecycleServiceWrapper struct { type lifecycleServiceWrapper struct {
Service SimpleLifecycle
name string name string
} }
func NewLifecycleService(service Service, name string) LifecycleService { func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
return &lifecycleServiceWrapper{ return &lifecycleServiceWrapper{
Service: service, SimpleLifecycle: service,
name: name, name: name,
} }
} }
@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
} }
func (l *lifecycleServiceWrapper) Start(stage StartStage) error { func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
return LegacyStart(l.Service, stage) return LegacyStart(l.SimpleLifecycle, stage)
} }
func (l *lifecycleServiceWrapper) Close() error { func (l *lifecycleServiceWrapper) Close() error {
return l.Service.Close() return l.SimpleLifecycle.Close()
} }

View File

@ -11,7 +11,7 @@ type HeadlessRule interface {
type Rule interface { type Rule interface {
HeadlessRule HeadlessRule
Service SimpleLifecycle
Type() string Type() string
Action() RuleAction Action() RuleAction
} }

View File

@ -1,6 +1,27 @@
package adapter package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface { type Service interface {
Start() error Lifecycle
Close() error Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
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
} }

View File

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

144
adapter/service/manager.go Normal file
View File

@ -0,0 +1,144 @@
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 {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
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 {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
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 {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
}
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:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}

View File

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

18
adapter/ssm.go Normal file
View File

@ -0,0 +1,18 @@
package adapter
import (
"net"
N "github.com/sagernet/sing/common/network"
)
type ManagedSSMServer interface {
Inbound
SetTracker(tracker SSMTracker)
UpdateUsers(users []string, uPSKs []string) error
}
type SSMTracker interface {
TrackConnection(conn net.Conn, metadata InboundContext) net.Conn
TrackPacketConnection(conn N.PacketConn, metadata InboundContext) N.PacketConn
}

View File

@ -3,6 +3,6 @@ package adapter
import "time" import "time"
type TimeService interface { type TimeService interface {
Service SimpleLifecycle
TimeFunc() func() time.Time TimeFunc() func() time.Time
} }

123
box.go
View File

@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/certificate" "github.com/sagernet/sing-box/common/certificate"
"github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/taskmonitor" "github.com/sagernet/sing-box/common/taskmonitor"
@ -34,22 +35,23 @@ import (
"github.com/sagernet/sing/service/pause" "github.com/sagernet/sing/service/pause"
) )
var _ adapter.Service = (*Box)(nil) var _ adapter.SimpleLifecycle = (*Box)(nil)
type Box struct { type Box struct {
createdAt time.Time createdAt time.Time
logFactory log.Factory logFactory log.Factory
logger log.ContextLogger logger log.ContextLogger
network *route.NetworkManager network *route.NetworkManager
endpoint *endpoint.Manager endpoint *endpoint.Manager
inbound *inbound.Manager inbound *inbound.Manager
outbound *outbound.Manager outbound *outbound.Manager
dnsTransport *dns.TransportManager service *boxService.Manager
dnsRouter *dns.Router dnsTransport *dns.TransportManager
connection *route.ConnectionManager dnsRouter *dns.Router
router *route.Router connection *route.ConnectionManager
services []adapter.LifecycleService router *route.Router
done chan struct{} internalService []adapter.LifecycleService
done chan struct{}
} }
type Options struct { type Options struct {
@ -64,6 +66,7 @@ func Context(
outboundRegistry adapter.OutboundRegistry, outboundRegistry adapter.OutboundRegistry,
endpointRegistry adapter.EndpointRegistry, endpointRegistry adapter.EndpointRegistry,
dnsTransportRegistry adapter.DNSTransportRegistry, dnsTransportRegistry adapter.DNSTransportRegistry,
serviceRegistry adapter.ServiceRegistry,
) context.Context { ) context.Context {
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
service.FromContext[adapter.InboundRegistry](ctx) == nil { service.FromContext[adapter.InboundRegistry](ctx) == nil {
@ -84,6 +87,10 @@ func Context(
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry) ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
} }
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
}
return ctx return ctx
} }
@ -99,6 +106,7 @@ func New(options Options) (*Box, error) {
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx) dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
if endpointRegistry == nil { if endpointRegistry == nil {
return nil, E.New("missing endpoint registry in context") return nil, E.New("missing endpoint registry in context")
@ -109,6 +117,12 @@ func New(options Options) (*Box, error) {
if outboundRegistry == nil { if outboundRegistry == nil {
return nil, E.New("missing outbound registry in context") return nil, E.New("missing outbound registry in context")
} }
if dnsTransportRegistry == nil {
return nil, E.New("missing DNS transport registry in context")
}
if serviceRegistry == nil {
return nil, E.New("missing service registry in context")
}
ctx = pause.WithDefaultManager(ctx) ctx = pause.WithDefaultManager(ctx)
experimentalOptions := common.PtrValueOrDefault(options.Experimental) experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@ -142,7 +156,7 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "create log factory") return nil, E.Cause(err, "create log factory")
} }
var services []adapter.LifecycleService var internalServices []adapter.LifecycleService
certificateOptions := common.PtrValueOrDefault(options.Certificate) certificateOptions := common.PtrValueOrDefault(options.Certificate)
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem || if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
len(certificateOptions.Certificate) > 0 || len(certificateOptions.Certificate) > 0 ||
@ -153,7 +167,7 @@ func New(options Options) (*Box, error) {
return nil, err return nil, err
} }
service.MustRegister[adapter.CertificateStore](ctx, certificateStore) service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
services = append(services, certificateStore) internalServices = append(internalServices, certificateStore)
} }
routeOptions := common.PtrValueOrDefault(options.Route) routeOptions := common.PtrValueOrDefault(options.Route)
@ -162,10 +176,12 @@ func New(options Options) (*Box, error) {
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final) dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager)
service.MustRegister[adapter.OutboundManager](ctx, outboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager) service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions) dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter) service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
@ -280,6 +296,24 @@ func New(options Options) (*Box, error) {
return nil, E.Cause(err, "initialize outbound[", i, "]") return nil, E.Cause(err, "initialize outbound[", i, "]")
} }
} }
for i, serviceOptions := range options.Services {
var tag string
if serviceOptions.Tag != "" {
tag = serviceOptions.Tag
} else {
tag = F.ToString(i)
}
err = serviceManager.Create(
ctx,
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
tag,
serviceOptions.Type,
serviceOptions.Options,
)
if err != nil {
return nil, E.Cause(err, "initialize service[", i, "]")
}
}
outboundManager.Initialize(common.Must1( outboundManager.Initialize(common.Must1(
direct.NewOutbound( direct.NewOutbound(
ctx, ctx,
@ -305,7 +339,7 @@ func New(options Options) (*Box, error) {
if needCacheFile { if needCacheFile {
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
service.MustRegister[adapter.CacheFile](ctx, cacheFile) service.MustRegister[adapter.CacheFile](ctx, cacheFile)
services = append(services, cacheFile) internalServices = append(internalServices, cacheFile)
} }
if needClashAPI { if needClashAPI {
clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@ -316,7 +350,7 @@ func New(options Options) (*Box, error) {
} }
router.AppendTracker(clashServer) router.AppendTracker(clashServer)
service.MustRegister[adapter.ClashServer](ctx, clashServer) service.MustRegister[adapter.ClashServer](ctx, clashServer)
services = append(services, clashServer) internalServices = append(internalServices, clashServer)
} }
if needV2RayAPI { if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@ -325,7 +359,7 @@ func New(options Options) (*Box, error) {
} }
if v2rayServer.StatsService() != nil { if v2rayServer.StatsService() != nil {
router.AppendTracker(v2rayServer.StatsService()) router.AppendTracker(v2rayServer.StatsService())
services = append(services, v2rayServer) internalServices = append(internalServices, v2rayServer)
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
} }
} }
@ -343,22 +377,23 @@ func New(options Options) (*Box, error) {
WriteToSystem: ntpOptions.WriteToSystem, WriteToSystem: ntpOptions.WriteToSystem,
}) })
timeService.TimeService = ntpService timeService.TimeService = ntpService
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
} }
return &Box{ return &Box{
network: networkManager, network: networkManager,
endpoint: endpointManager, endpoint: endpointManager,
inbound: inboundManager, inbound: inboundManager,
outbound: outboundManager, outbound: outboundManager,
dnsTransport: dnsTransportManager, dnsTransport: dnsTransportManager,
dnsRouter: dnsRouter, service: serviceManager,
connection: connectionManager, dnsRouter: dnsRouter,
router: router, connection: connectionManager,
createdAt: createdAt, router: router,
logFactory: logFactory, createdAt: createdAt,
logger: logFactory.Logger(), logFactory: logFactory,
services: services, logger: logFactory.Logger(),
done: make(chan struct{}), internalService: internalServices,
done: make(chan struct{}),
}, nil }, nil
} }
@ -408,11 +443,11 @@ func (s *Box) preStart() error {
if err != nil { if err != nil {
return E.Cause(err, "start logger") return E.Cause(err, "start logger")
} }
err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
@ -428,31 +463,27 @@ func (s *Box) start() error {
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStateStart, s.services) err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = s.inbound.Start(adapter.StartStateStart) err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStart, s.endpoint) err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
if err != nil { if err != nil {
return err return err
} }
err = adapter.StartNamed(adapter.StartStatePostStart, s.services) err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
if err != nil { if err != nil {
return err return err
} }
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint) err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
if err != nil {
return err
}
err = adapter.StartNamed(adapter.StartStateStarted, s.services)
if err != nil { if err != nil {
return err return err
} }
@ -469,7 +500,7 @@ func (s *Box) Close() error {
err := common.Close( err := common.Close(
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network, s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
) )
for _, lifecycleService := range s.services { for _, lifecycleService := range s.internalService {
err = E.Append(err, lifecycleService.Close(), func(err error) error { err = E.Append(err, lifecycleService.Close(), func(err error) error {
return E.Cause(err, "close ", lifecycleService.Name()) return E.Cause(err, "close ", lifecycleService.Name())
}) })

View File

@ -105,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
tag := tagVersion.VersionString() tag := tagVersion.VersionString()
client := createClient(10 * time.Minute) client := createClient(20 * time.Minute)
log.Info(tag, " list build IDs") log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil) buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
@ -145,7 +145,7 @@ func publishTestflight(ctx context.Context) error {
return err return err
} }
build := builds.Data[0] build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute { if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process") log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second) time.Sleep(15 * time.Second)
continue continue

View File

@ -59,8 +59,8 @@ func init() {
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=") sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag) debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api") sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack") iosTags = append(iosTags, "with_dhcp", "with_low_memory")
memcTags = append(memcTags, "with_tailscale") memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug") debugTags = append(debugTags, "debug")
} }

View File

@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@ -68,6 +67,5 @@ func preRun(cmd *cobra.Command, args []string) {
if len(configPaths) == 0 && len(configDirectories) == 0 { if len(configPaths) == 0 && len(configDirectories) == 0 {
configPaths = append(configPaths, "config.json") configPaths = append(configPaths, "config.json")
} }
globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) globalCtx = include.Context(service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())))
globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
} }

View File

@ -66,11 +66,17 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
interfaceFinder = control.NewDefaultInterfaceFinder() interfaceFinder = control.NewDefaultInterfaceFinder()
} }
if options.BindInterface != "" { if options.BindInterface != "" {
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark > 0 { if options.RoutingMark > 0 {
if !C.IsLinux {
return nil, E.New("`routing_mark` is only supported on Linux")
}
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
} }

View File

@ -32,6 +32,7 @@ type Listener struct {
disablePacketOutput bool disablePacketOutput bool
setSystemProxy bool setSystemProxy bool
systemProxySOCKS bool systemProxySOCKS bool
tproxy bool
tcpListener net.Listener tcpListener net.Listener
systemProxy settings.SystemProxy systemProxy settings.SystemProxy
@ -54,6 +55,7 @@ type Options struct {
DisablePacketOutput bool DisablePacketOutput bool
SetSystemProxy bool SetSystemProxy bool
SystemProxySOCKS bool SystemProxySOCKS bool
TProxy bool
} }
func New( func New(
@ -71,6 +73,7 @@ func New(
disablePacketOutput: options.DisablePacketOutput, disablePacketOutput: options.DisablePacketOutput,
setSystemProxy: options.SetSystemProxy, setSystemProxy: options.SetSystemProxy,
systemProxySOCKS: options.SystemProxySOCKS, systemProxySOCKS: options.SystemProxySOCKS,
tproxy: options.TProxy,
} }
} }

View File

@ -3,9 +3,11 @@ package listener
import ( import (
"net" "net"
"net/netip" "net/netip"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
@ -51,6 +53,13 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
} }
setMultiPathTCP(&listenConfig) setMultiPathTCP(&listenConfig)
} }
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), false)
})
})
}
tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) { tcpListener, err := ListenNetworkNamespace[net.Listener](l.listenOptions.NetNs, func() (net.Listener, error) {
if l.listenOptions.TCPFastOpen { if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig var tfoConfig tfo.ListenConfig

View File

@ -5,8 +5,10 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"syscall"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -36,6 +38,13 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if !udpFragment { if !udpFragment {
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment()) listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
} }
if l.tproxy {
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
return redir.TProxy(fd, !M.ParseSocksaddr(address).IsIPv4(), true)
})
})
}
udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) { udpConn, err := ListenNetworkNamespace[net.PacketConn](l.listenOptions.NetNs, func() (net.PacketConn, error) {
return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) return listenConfig.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String())
}) })

View File

@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err == nil { if err == nil {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
@ -20,11 +20,13 @@ func TProxy(fd uintptr, isIPv6 bool) error {
if err == nil && isIPv6 { if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1) err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
} }
if err == nil { if isUDP {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1) if err == nil {
} err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1)
if err == nil && isIPv6 { }
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1) if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_RECVORIGDSTADDR, 1)
}
} }
return err return err
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
) )
func TProxy(fd uintptr, isIPv6 bool) error { func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
return os.ErrInvalid return os.ErrInvalid
} }

View File

@ -37,7 +37,7 @@ func (w *acmeWrapper) Close() error {
return nil return nil
} }
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
var acmeServer string var acmeServer string
switch options.Provider { switch options.Provider {
case "", "letsencrypt": case "", "letsencrypt":

View File

@ -11,6 +11,6 @@ import (
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) { func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`) return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
} }

View File

@ -22,7 +22,7 @@ var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct { type STDServerConfig struct {
config *tls.Config config *tls.Config
logger log.Logger logger log.Logger
acmeService adapter.Service acmeService adapter.SimpleLifecycle
certificate []byte certificate []byte
key []byte key []byte
certificatePath string certificatePath string
@ -165,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, nil return nil, nil
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
var acmeService adapter.Service var acmeService adapter.SimpleLifecycle
var err error var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 { if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck //nolint:staticcheck

View File

@ -1,7 +1,9 @@
package tf package tf
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"math/rand" "math/rand"
"net" "net"
"strings" "strings"
@ -17,17 +19,19 @@ type Conn struct {
tcpConn *net.TCPConn tcpConn *net.TCPConn
ctx context.Context ctx context.Context
firstPacketWritten bool firstPacketWritten bool
splitRecord bool
fallbackDelay time.Duration fallbackDelay time.Duration
} }
func NewConn(conn net.Conn, ctx context.Context, fallbackDelay time.Duration) (*Conn, error) { func NewConn(conn net.Conn, ctx context.Context, splitRecord bool, fallbackDelay time.Duration) *Conn {
tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn) tcpConn, _ := N.UnwrapReader(conn).(*net.TCPConn)
return &Conn{ return &Conn{
Conn: conn, Conn: conn,
tcpConn: tcpConn, tcpConn: tcpConn,
ctx: ctx, ctx: ctx,
splitRecord: splitRecord,
fallbackDelay: fallbackDelay, fallbackDelay: fallbackDelay,
}, nil }
} }
func (c *Conn) Write(b []byte) (n int, err error) { func (c *Conn) Write(b []byte) (n int, err error) {
@ -37,10 +41,12 @@ func (c *Conn) Write(b []byte) (n int, err error) {
}() }()
serverName := indexTLSServerName(b) serverName := indexTLSServerName(b)
if serverName != nil { if serverName != nil {
if c.tcpConn != nil { if !c.splitRecord {
err = c.tcpConn.SetNoDelay(true) if c.tcpConn != nil {
if err != nil { err = c.tcpConn.SetNoDelay(true)
return if err != nil {
return
}
} }
} }
splits := strings.Split(serverName.ServerName, ".") splits := strings.Split(serverName.ServerName, ".")
@ -61,16 +67,25 @@ func (c *Conn) Write(b []byte) (n int, err error) {
currentIndex++ currentIndex++
} }
} }
var buffer bytes.Buffer
for i := 0; i <= len(splitIndexes); i++ { for i := 0; i <= len(splitIndexes); i++ {
var payload []byte var payload []byte
if i == 0 { if i == 0 {
payload = b[:splitIndexes[i]] payload = b[:splitIndexes[i]]
if c.splitRecord {
payload = payload[recordLayerHeaderLen:]
}
} else if i == len(splitIndexes) { } else if i == len(splitIndexes) {
payload = b[splitIndexes[i-1]:] payload = b[splitIndexes[i-1]:]
} else { } else {
payload = b[splitIndexes[i-1]:splitIndexes[i]] payload = b[splitIndexes[i-1]:splitIndexes[i]]
} }
if c.tcpConn != nil && i != len(splitIndexes) { if c.splitRecord {
payloadLen := uint16(len(payload))
buffer.Write(b[:3])
binary.Write(&buffer, binary.BigEndian, payloadLen)
buffer.Write(payload)
} else if c.tcpConn != nil && i != len(splitIndexes) {
err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay) err = writeAndWaitAck(c.ctx, c.tcpConn, payload, c.fallbackDelay)
if err != nil { if err != nil {
return return
@ -82,11 +97,18 @@ func (c *Conn) Write(b []byte) (n int, err error) {
} }
} }
} }
if c.tcpConn != nil { if c.splitRecord {
err = c.tcpConn.SetNoDelay(false) _, err = c.Conn.Write(buffer.Bytes())
if err != nil { if err != nil {
return return
} }
} else {
if c.tcpConn != nil {
err = c.tcpConn.SetNoDelay(false)
if err != nil {
return
}
}
} }
return len(b), nil return len(b), nil
} }

View File

@ -0,0 +1,32 @@
package tf_test
import (
"context"
"crypto/tls"
"net"
"testing"
tf "github.com/sagernet/sing-box/common/tlsfragment"
"github.com/stretchr/testify/require"
)
func TestTLSFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), false, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}
func TestTLSRecordFragment(t *testing.T) {
t.Parallel()
tcpConn, err := net.Dial("tcp", "1.1.1.1:443")
require.NoError(t, err)
tlsConn := tls.Client(tf.NewConn(tcpConn, context.Background(), true, 0), &tls.Config{
ServerName: "www.cloudflare.com",
})
require.NoError(t, tlsConn.Handshake())
}

View File

@ -25,6 +25,9 @@ const (
TypeTUIC = "tuic" TypeTUIC = "tuic"
TypeHysteria2 = "hysteria2" TypeHysteria2 = "hysteria2"
TypeTailscale = "tailscale" TypeTailscale = "tailscale"
TypeDERP = "derp"
TypeResolved = "resolved"
TypeSSMAPI = "ssm-api"
) )
const ( const (

View File

@ -34,6 +34,7 @@ type Client struct {
disableCache bool disableCache bool
disableExpire bool disableExpire bool
independentCache bool independentCache bool
clientSubnet netip.Prefix
rdrc adapter.RDRCStore rdrc adapter.RDRCStore
initRDRCFunc func() adapter.RDRCStore initRDRCFunc func() adapter.RDRCStore
logger logger.ContextLogger logger logger.ContextLogger
@ -47,6 +48,7 @@ type ClientOptions struct {
DisableExpire bool DisableExpire bool
IndependentCache bool IndependentCache bool
CacheCapacity uint32 CacheCapacity uint32
ClientSubnet netip.Prefix
RDRC func() adapter.RDRCStore RDRC func() adapter.RDRCStore
Logger logger.ContextLogger Logger logger.ContextLogger
} }
@ -57,6 +59,7 @@ func NewClient(options ClientOptions) *Client {
disableCache: options.DisableCache, disableCache: options.DisableCache,
disableExpire: options.DisableExpire, disableExpire: options.DisableExpire,
independentCache: options.IndependentCache, independentCache: options.IndependentCache,
clientSubnet: options.ClientSubnet,
initRDRCFunc: options.RDRC, initRDRCFunc: options.RDRC,
logger: options.Logger, logger: options.Logger,
} }
@ -104,8 +107,12 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
return &responseMessage, nil return &responseMessage, nil
} }
question := message.Question[0] question := message.Question[0]
if options.ClientSubnet.IsValid() { clientSubnet := options.ClientSubnet
message = SetClientSubnet(message, options.ClientSubnet) if !clientSubnet.IsValid() {
clientSubnet = c.clientSubnet
}
if clientSubnet.IsValid() {
message = SetClientSubnet(message, clientSubnet)
} }
isSimpleRequest := len(message.Question) == 1 && isSimpleRequest := len(message.Question) == 1 &&
len(message.Ns) == 0 && len(message.Ns) == 0 &&
@ -253,9 +260,15 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) { func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
domain = FqdnToDomain(domain) domain = FqdnToDomain(domain)
dnsName := dns.Fqdn(domain) dnsName := dns.Fqdn(domain)
if options.Strategy == C.DomainStrategyIPv4Only { var strategy C.DomainStrategy
if options.LookupStrategy != C.DomainStrategyAsIS {
strategy = options.LookupStrategy
} else {
strategy = options.Strategy
}
if strategy == C.DomainStrategyIPv4Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeA, options, responseChecker)
} else if options.Strategy == C.DomainStrategyIPv6Only { } else if strategy == C.DomainStrategyIPv6Only {
return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker) return c.lookupToExchange(ctx, transport, dnsName, dns.TypeAAAA, options, responseChecker)
} }
var response4 []netip.Addr var response4 []netip.Addr
@ -281,7 +294,7 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
if len(response4) == 0 && len(response6) == 0 { if len(response4) == 0 && len(response6) == 0 {
return nil, err return nil, err
} }
return sortAddresses(response4, response6, options.Strategy), nil return sortAddresses(response4, response6, strategy), nil
} }
func (c *Client) ClearCache() { func (c *Client) ClearCache() {
@ -537,12 +550,26 @@ func transportTagFromContext(ctx context.Context) (string, bool) {
return value, loaded return value, loaded
} }
func FixedResponseStatus(message *dns.Msg, rcode int) *dns.Msg {
return &dns.Msg{
MsgHdr: dns.MsgHdr{
Id: message.Id,
Rcode: rcode,
Response: true,
},
Question: message.Question,
}
}
func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg { func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }
@ -575,9 +602,12 @@ func FixedResponse(id uint16, question dns.Question, addresses []netip.Addr, tim
func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg { func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
Answer: []dns.RR{ Answer: []dns.RR{
@ -598,9 +628,12 @@ func FixedResponseCNAME(id uint16, question dns.Question, record string, timeToL
func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg { func FixedResponseTXT(id uint16, question dns.Question, records []string, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
Answer: []dns.RR{ Answer: []dns.RR{
@ -621,9 +654,12 @@ func FixedResponseTXT(id uint16, question dns.Question, records []string, timeTo
func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg { func FixedResponseMX(id uint16, question dns.Question, records []*net.MX, timeToLive uint32) *dns.Msg {
response := dns.Msg{ response := dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Id: id, Id: id,
Rcode: dns.RcodeSuccess, Response: true,
Response: true, Authoritative: true,
RecursionDesired: true,
RecursionAvailable: true,
Rcode: dns.RcodeSuccess,
}, },
Question: []dns.Question{question}, Question: []dns.Question{question},
} }

View File

@ -55,6 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
DisableExpire: options.DNSClientOptions.DisableExpire, DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache, IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity, CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore { RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil { if cacheFile == nil {
@ -292,7 +293,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} else if errors.Is(err, ErrResponseRejected) { } else if errors.Is(err, ErrResponseRejected) {
rejected = true rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 { } else if len(message.Question) > 0 {
rejected = true
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>")) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))

View File

@ -60,13 +60,17 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
if !serverAddr.IsValid() { if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr) return nil, E.New("invalid server address: ", serverAddr)
} }
return NewTLSRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), transportDialer, serverAddr, tlsConfig), nil
}
func NewTLSRaw(logger logger.ContextLogger, adapter dns.TransportAdapter, dialer N.Dialer, serverAddr M.Socksaddr, tlsConfig tls.Config) *TLSTransport {
return &TLSTransport{ return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions), TransportAdapter: adapter,
logger: logger, logger: logger,
dialer: transportDialer, dialer: dialer,
serverAddr: serverAddr, serverAddr: serverAddr,
tlsConfig: tlsConfig, tlsConfig: tlsConfig,
}, nil }
} }
func (t *TLSTransport) Start(stage adapter.StartStage) error { func (t *TLSTransport) Start(stage adapter.StartStage) error {

View File

@ -2,6 +2,21 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-beta.22
* Fixes and improvements
#### 1.12.0-beta.21
* Fix missing `home` option for DERP service **1**
* Fixes and improvements
**1**:
You can now choose what the DERP home page shows, just like with derper's `-home` flag.
See [DERP](/configuration/service/derp/#home).
### 1.11.13 ### 1.11.13
* Fixes and improvements * Fixes and improvements
@ -9,6 +24,37 @@ icon: material/alert-decagram
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.17
* Update quic-go to v0.52.0
* Fixes and improvements
#### 1.12.0-beta.15
* Add DERP service **1**
* Add Resolved service and DNS server **2**
* Add SSM API service **3**
* Fixes and improvements
**1**:
DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper).
See [DERP Service](/configuration/service/derp/).
**2**:
Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs
(e.g. NetworkManager) and provide DNS resolution.
See [Resolved Service](/configuration/service/resolved/) and [Resolved DNS Server](/configuration/dns/server/resolved/).
**3**:
SSM API service is a RESTful API server for managing Shadowsocks servers.
See [SSM API Service](/configuration/service/ssm-api/).
### 1.11.11 ### 1.11.11
* Fixes and improvements * Fixes and improvements
@ -16,6 +62,31 @@ violated the rules (TestFlight users are not affected)._
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.13
* Add TLS record fragment route options **1**
* Add missing `accept_routes` option for Tailscale **2**
* Fixes and improvements
**1**:
See [Route Action](/configuration/route/rule_action/#tls_record_fragment).
**2**:
See [Tailscale](/configuration/endpoint/tailscale/#accept_routes).
#### 1.12.0-beta.10
* Add control options for listeners **1**
* Fixes and improvements
**1**:
You can now set `bind_interface`, `routing_mark` and `reuse_addr` in Listen Fields.
See [Listen Fields](/configuration/shared/listen/).
### 1.11.10 ### 1.11.10
* Undeprecate the `block` outbound **1** * Undeprecate the `block` outbound **1**
@ -29,6 +100,11 @@ we decided to temporarily undeprecate the `block` outbound until a replacement i
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.9
* Update quic-go to v0.51.0
* Fixes and improvements
### 1.11.9 ### 1.11.9
* Fixes and improvements * Fixes and improvements
@ -36,6 +112,10 @@ violated the rules (TestFlight users are not affected)._
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.5
* Fixes and improvements
### 1.11.8 ### 1.11.8
* Improve `auto_redirect` **1** * Improve `auto_redirect` **1**
@ -49,6 +129,10 @@ see [Tun](/configuration/inbound/tun/#auto_redirect).
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.3
* Fixes and improvements
### 1.11.7 ### 1.11.7
* Fixes and improvements * Fixes and improvements
@ -56,6 +140,15 @@ violated the rules (TestFlight users are not affected)._
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.1
* Fixes and improvements
**1**:
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
see [Tun](/configuration/inbound/tun/#auto_redirect).
### 1.11.6 ### 1.11.6
* Fixes and improvements * Fixes and improvements
@ -63,6 +156,40 @@ violated the rules (TestFlight users are not affected)._
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.19
* Update gVisor to 20250319.0
* Fixes and improvements
#### 1.12.0-alpha.18
* Add wildcard SNI support for ShadowTLS inbound **1**
* Fixes and improvements
**1**:
See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni).
#### 1.12.0-alpha.17
* Add NTP sniffer **1**
* Fixes and improvements
**1**:
See [Protocol Sniff](/configuration/route/sniff/).
#### 1.12.0-alpha.16
* Update `domain_resolver` behavior **1**
* Fixes and improvements
**1**:
`route.default_domain_resolver` or `outbound.domain_resolver` is now optional when only one DNS server is configured.
See [Dial Fields](/configuration/shared/dial/#domain_resolver).
### 1.11.5 ### 1.11.5
* Fixes and improvements * Fixes and improvements
@ -70,10 +197,71 @@ violated the rules (TestFlight users are not affected)._
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we _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)._ violated the rules (TestFlight users are not affected)._
#### 1.12.0-alpha.13
* Move `predefined` DNS server to DNS rule action **1**
* Fixes and improvements
**1**:
See [DNS Rule Action](/configuration/dns/rule_action/#predefined).
### 1.11.4 ### 1.11.4
* Fixes and improvements * Fixes and improvements
#### 1.12.0-alpha.11
* Fixes and improvements
#### 1.12.0-alpha.10
* Add AnyTLS protocol **1**
* Improve `resolve` route action **2**
* Migrate to stdlib ECH implementation **3**
* Fixes and improvements
**1**:
The new AnyTLS protocol claims to mitigate TLS proxy traffic characteristics and comes with a new multiplexing scheme.
See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/configuration/outbound/anytls/).
**2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
**3**:
See [TLS](/configuration/shared/tls).
The build tag `with_ech` is no longer needed and has been removed.
#### 1.12.0-alpha.7
* Add Tailscale DNS server **1**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/dns/server/tailscale/).
#### 1.12.0-alpha.6
* Add Tailscale endpoint **1**
* Drop support for go1.22 **2**
* Fixes and improvements
**1**:
See [Tailscale](/configuration/endpoint/tailscale/).
**2**:
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3 ### 1.11.3
* Fixes and improvements * Fixes and improvements
@ -81,10 +269,69 @@ violated the rules (TestFlight users are not affected)._
_This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration _This version overwrites 1.11.2, as incorrect binaries were released due to a bug in the continuous integration
process._ process._
#### 1.12.0-alpha.5
* Fixes and improvements
### 1.11.1 ### 1.11.1
* Fixes and improvements * Fixes and improvements
#### 1.12.0-alpha.2
* Update quic-go to v0.49.0
* Fixes and improvements
#### 1.12.0-alpha.1
* Refactor DNS servers **1**
* Add domain resolver options**2**
* Add TLS fragment route options **3**
* Add certificate options **4**
**1**:
DNS servers are refactored for better performance and scalability.
See [DNS server](/configuration/dns/server/).
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
Compatibility for old formats will be removed in sing-box 1.14.0.
**2**:
Legacy `outbound` DNS rules are deprecated
and can be replaced by the new `domain_resolver` option.
See [Dial Fields](/configuration/shared/dial/#domain_resolver) and
[Route](/configuration/route/#default_domain_resolver).
For migration,
see [Migrate outbound DNS rule items to domain resolver](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver).
**3**:
The new TLS fragment route options allow you to fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used
to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are
known to be blocked.
See [Route Action](/configuration/route/rule_action/#tls_fragment).
**4**:
New certificate options allow you to manage the default list of trusted X509 CA certificates.
For the system certificate list, fixed Go not reading Android trusted certificates correctly.
You can also use the Mozilla Included List instead, or add trusted certificates yourself.
See [Certificate](/configuration/certificate/).
### 1.11.0 ### 1.11.0
Important changes since 1.10: Important changes since 1.10:

View File

@ -1,7 +1,11 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "Changes in sing-box 1.12.0"
:material-decagram: [servers](#servers)
!!! quote "Changes in sing-box 1.11.0" !!! quote "Changes in sing-box 1.11.0"
:material-plus: [cache_capacity](#cache_capacity) :material-plus: [cache_capacity](#cache_capacity)

View File

@ -1,7 +1,11 @@
--- ---
icon: material/new-box icon: material/alert-decagram
--- ---
!!! quote "sing-box 1.12.0 中的更改"
:material-decagram: [servers](#servers)
!!! quote "sing-box 1.11.0 中的更改" !!! quote "sing-box 1.11.0 中的更改"
:material-plus: [cache_capacity](#cache_capacity) :material-plus: [cache_capacity](#cache_capacity)

View File

@ -81,7 +81,7 @@ Will overrides `dns.client_subnet`.
#### method #### method
- `default`: Reply with NXDOMAIN. - `default`: Reply with REFUSED.
- `drop`: Drop the request. - `drop`: Drop the request.
`default` will be used by default. `default` will be used by default.

View File

@ -81,7 +81,7 @@ icon: material/new-box
#### method #### method
- `default`: 返回 NXDOMAIN - `default`: 返回 REFUSED
- `drop`: 丢弃请求。 - `drop`: 丢弃请求。
默认使用 `defualt` 默认使用 `defualt`

View File

@ -41,6 +41,7 @@ The type of the DNS server.
| `dhcp` | [DHCP](./dhcp/) | | `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag #### tag

View File

@ -41,6 +41,7 @@ DNS 服务器的类型。
| `dhcp` | [DHCP](./dhcp/) | | `dhcp` | [DHCP](./dhcp/) |
| `fakeip` | [Fake IP](./fakeip/) | | `fakeip` | [Fake IP](./fakeip/) |
| `tailscale` | [Tailscale](./tailscale/) | | `tailscale` | [Tailscale](./tailscale/) |
| `resolved` | [Resolved](./resolved/) |
#### tag #### tag

View File

@ -0,0 +1,84 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Resolved
```json
{
"dns": {
"servers": [
{
"type": "resolved",
"tag": "",
"service": "resolved",
"accept_default_resolvers": false
}
]
}
}
```
### Fields
#### service
==Required==
The tag of the [Resolved Service](/configuration/service/resolved).
#### accept_default_resolvers
Indicates whether the default DNS resolvers should be accepted for fallback queries in addition to matching domains.
Specifically, default DNS resolvers are DNS servers that have `SetLinkDefaultRoute` or `SetLinkDomains ~.` set.
If not enabled, `NXDOMAIN` will be returned for requests that do not match search or match domains.
### Examples
=== "Split DNS only"
```json
{
"dns": {
"servers": [
{
"type": "local",
"tag": "local"
},
{
"type": "resolved",
"tag": "resolved",
"service": "resolved"
}
],
"rules": [
{
"ip_accept_any": true,
"server": "resolved"
}
]
}
}
```
=== "Use as global DNS"
```json
{
"dns": {
"servers": [
{
"type": "resolved",
"service": "resolved",
"accept_default_resolvers": true
}
]
}
}
```

View File

@ -30,13 +30,13 @@ icon: material/new-box
==Required== ==Required==
The tag of the Tailscale endpoint. The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale).
#### accept_default_resolvers #### accept_default_resolvers
Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。 Indicates whether default DNS resolvers should be accepted for fallback queries in addition to MagicDNS。
if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries. if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
### Examples ### Examples
@ -80,4 +80,4 @@ if not enabled, NXDOMAIN will be returned for non-Tailscale domain queries.
] ]
} }
} }
``` ```

View File

@ -6,7 +6,7 @@ icon: material/new-box
# Endpoint # Endpoint
Endpoint is protocols that has both inbound and outbound behavior. An endpoint is a protocol with inbound and outbound behavior.
### Structure ### Structure

View File

@ -15,6 +15,7 @@ icon: material/new-box
"control_url": "", "control_url": "",
"ephemeral": false, "ephemeral": false,
"hostname": "", "hostname": "",
"accept_routes": false,
"exit_node": "", "exit_node": "",
"exit_node_allow_lan_access": false, "exit_node_allow_lan_access": false,
"advertise_routes": [], "advertise_routes": [],
@ -62,6 +63,10 @@ System hostname is used by default.
Example: `localhost` Example: `localhost`
#### accept_routes
Indicates whether the node should accept routes advertised by other nodes.
#### exit_node #### exit_node
The exit node name or IP address to use. The exit node name or IP address to use.

View File

@ -14,6 +14,7 @@ sing-box uses JSON for configuration files.
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
"services": [],
"experimental": {} "experimental": {}
} }
``` ```
@ -30,6 +31,7 @@ sing-box uses JSON for configuration files.
| `inbounds` | [Inbound](./inbound/) | | `inbounds` | [Inbound](./inbound/) |
| `outbounds` | [Outbound](./outbound/) | | `outbounds` | [Outbound](./outbound/) |
| `route` | [Route](./route/) | | `route` | [Route](./route/) |
| `services` | [Service](./service/) |
| `experimental` | [Experimental](./experimental/) | | `experimental` | [Experimental](./experimental/) |
### Check ### Check

View File

@ -14,6 +14,7 @@ sing-box 使用 JSON 作为配置文件格式。
"inbounds": [], "inbounds": [],
"outbounds": [], "outbounds": [],
"route": {}, "route": {},
"services": [],
"experimental": {} "experimental": {}
} }
``` ```
@ -30,6 +31,7 @@ sing-box 使用 JSON 作为配置文件格式。
| `inbounds` | [入站](./inbound/) | | `inbounds` | [入站](./inbound/) |
| `outbounds` | [出站](./outbound/) | | `outbounds` | [出站](./outbound/) |
| `route` | [路由](./route/) | | `route` | [路由](./route/) |
| `services` | [服务](./service/) |
| `experimental` | [实验性](./experimental/) | | `experimental` | [实验性](./experimental/) |
### 检查 ### 检查

View File

@ -6,6 +6,7 @@ icon: material/new-box
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
:material-plus: [tls_record_fragment](#tls_record_fragment)
:material-plus: [resolve.disable_cache](#disable_cache) :material-plus: [resolve.disable_cache](#disable_cache)
:material-plus: [resolve.rewrite_ttl](#rewrite_ttl) :material-plus: [resolve.rewrite_ttl](#rewrite_ttl)
:material-plus: [resolve.client_subnet](#client_subnet) :material-plus: [resolve.client_subnet](#client_subnet)
@ -91,7 +92,8 @@ Not available when `method` is set to drop.
"udp_connect": false, "udp_connect": false,
"udp_timeout": "", "udp_timeout": "",
"tls_fragment": false, "tls_fragment": false,
"tls_fragment_fallback_delay": "" "tls_fragment_fallback_delay": "",
"tls_record_fragment": ""
} }
``` ```
@ -164,13 +166,19 @@ If no protocol is sniffed, the following ports will be recognized as protocols b
Fragment TLS handshakes to bypass firewalls. Fragment TLS handshakes to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**, and should not be used to circumvent real censorship. This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
and should not be used to circumvent real censorship.
Since it is not designed for performance, it should not be applied to all connections, but only to server names that are known to be blocked. Due to poor performance, try `tls_record_fragment` first, and only apply to server names known to be blocked.
On Linux, Apple platforms, (administrator privileges required) Windows, the wait time can be automatically detected, otherwise it will fall back to waiting for a fixed time specified by `tls_fragment_fallback_delay`. On Linux, Apple platforms, (administrator privileges required) Windows,
the wait time can be automatically detected, otherwise it will fall back to
waiting for a fixed time specified by `tls_fragment_fallback_delay`.
In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time, because the target is considered to be local or behind a transparent proxy. In addition, if the actual wait time is less than 20ms, it will also fall back to waiting for a fixed time,
because the target is considered to be local or behind a transparent proxy.
Conflict with `tls_record_fragment`.
#### tls_fragment_fallback_delay #### tls_fragment_fallback_delay
@ -180,6 +188,17 @@ The fallback value used when TLS segmentation cannot automatically determine the
`500ms` is used by default. `500ms` is used by default.
#### tls_record_fragment
!!! question "Since sing-box 1.12.0"
Fragment TLS handshake into multiple TLS records to bypass firewalls.
This feature is intended to circumvent simple firewalls based on **plaintext packet matching**,
and should not be used to circumvent real censorship.
Conflict with `tls_fragment`.
### sniff ### sniff
```json ```json

View File

@ -5,7 +5,11 @@ icon: material/new-box
!!! quote "sing-box 1.12.0 中的更改" !!! quote "sing-box 1.12.0 中的更改"
:material-plus: [tls_fragment](#tls_fragment) :material-plus: [tls_fragment](#tls_fragment)
:material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay) :material-plus: [tls_fragment_fallback_delay](#tls_fragment_fallback_delay)
:material-plus: [tls_record_fragment](#tls_record_fragment)
:material-plus: [resolve.disable_cache](#disable_cache)
:material-plus: [resolve.rewrite_ttl](#rewrite_ttl)
:material-plus: [resolve.client_subnet](#client_subnet)
## 最终动作 ## 最终动作
@ -159,12 +163,15 @@ UDP 连接超时时间。
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。 此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
由于它不是为性能设计的,不应被应用于所有连接,而仅应用于已知被阻止的服务器名称。 由于性能不佳,请首先尝试 `tls_record_fragment`,且仅应用于已知被阻止的服务器名称。
在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。 在 Linux、Apple 平台和需要管理员权限的 Windows 系统上,可自动检测等待时间。
若无法自动检测,将回退使用 `tls_fragment_fallback_delay` 指定的固定等待时间。
此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。 此外,若实际等待时间小于 20 毫秒,同样会回退至固定等待时间模式,因为此时判定目标处于本地或透明代理之后。
`tls_record_fragment` 冲突。
#### tls_fragment_fallback_delay #### tls_fragment_fallback_delay
!!! question "自 sing-box 1.12.0 起" !!! question "自 sing-box 1.12.0 起"
@ -173,6 +180,16 @@ UDP 连接超时时间。
默认使用 `500ms` 默认使用 `500ms`
#### tls_record_fragment
!!! question "自 sing-box 1.12.0 起"
通过分段 TLS 握手数据包到多个 TLS 记录来绕过防火墙检测。
此功能旨在规避基于**明文数据包匹配**的简单防火墙,不应该用于规避真的审查。
`tls_fragment` 冲突。
### sniff ### sniff
```json ```json

View File

@ -0,0 +1,135 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# DERP
DERP service is a Tailscale DERP server, similar to [derper](https://pkg.go.dev/tailscale.com/cmd/derper).
### Structure
```json
{
"type": "derp",
... // Listen Fields
"tls": {},
"config_path": "",
"verify_client_endpoint": [],
"verify_client_url": [],
"home": "",
"mesh_with": [],
"mesh_psk": "",
"mesh_psk_file": "",
"stun": {}
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).
#### config_path
==Required==
Derper configuration file path.
Example: `derper.key`
#### verify_client_endpoint
Tailscale endpoints tags to verify clients.
#### verify_client_url
URL to verify clients.
Object format:
```json
{
"url": "https://my-headscale.com/verify",
... // Dial Fields
}
```
Setting Array value to a string `__URL__` is equivalent to configuring:
```json
{ "url": __URL__ }
```
#### home
What to serve at the root path. It may be left empty (the default, for a default homepage), `blank` for a blank page, or a URL to redirect to
#### mesh_with
Mesh with other DERP servers.
Object format:
```json
{
"server": "",
"server_port": "",
"host": "",
"tls": {},
... // Dial Fields
}
```
Object fields:
- `server`: **Required** DERP server address.
- `server_port`: **Required** DERP server port.
- `host`: Custom DERP hostname.
- `tls`: [TLS](/configuration/shared/tls/#outbound)
- `Dial Fields`: [Dial Fields](/configuration/shared/dial/)
#### mesh_psk
Pre-shared key for DERP mesh.
#### mesh_psk_file
Pre-shared key file for DERP mesh.
#### stun
STUN server listen options.
Object format:
```json
{
"enabled": true,
... // Listen Fields
}
```
Object fields:
- `enabled`: **Required** Enable STUN server.
- `listen`: **Required** STUN server listen address, default to `::`.
- `listen_port`: **Required** STUN server listen port, default to `3478`.
- `other Listen Fields`: [Listen Fields](/configuration/shared/listen/)
Setting `stun` value to a number `__PORT__` is equivalent to configuring:
```json
{ "enabled": true, "listen_port": __PORT__ }
```

View File

@ -0,0 +1,32 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Service
### Structure
```json
{
"services": [
{
"type": "",
"tag": ""
}
]
}
```
### Fields
| Type | Format |
|------------|------------------------|
| `derp` | [DERP](./derp) |
| `resolved` | [Resolved](./resolved) |
| `ssm-api` | [SSM API](./ssm-api) |
#### tag
The tag of the endpoint.

View File

@ -0,0 +1,44 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# Resolved
Resolved service is a fake systemd-resolved DBUS service to receive DNS settings from other programs
(e.g. NetworkManager) and provide DNS resolution.
See also: [Resolved DNS Server](/configuration/dns/server/resolved/)
### Structure
```json
{
"type": "resolved",
... // Listen Fields
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### listen
==Required==
Listen address.
`127.0.0.53` will be used by default.
#### listen_port
==Required==
Listen port.
`53` will be used by default.

View File

@ -0,0 +1,52 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.12.0"
# SSM API
SSM API service is a RESTful API server for managing Shadowsocks servers.
See https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2023-1-shadowsocks-server-management-api-v1.md
### Structure
```json
{
"type": "ssm-api",
... // Listen Fields
"servers": {},
"tls": {}
}
```
### Listen Fields
See [Listen Fields](/configuration/shared/listen/) for details.
### Fields
#### servers
==Required==
A mapping Object from HTTP endpoints to [Shadowsocks Inbound](/configuration/inbound/shadowsocks) tags.
Selected Shadowsocks inbounds must be configured with [managed](/configuration/inbound/shadowsocks#managed) enabled.
Example:
```json
{
"servers": {
"/": "ss-in"
}
}
```
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@ -33,7 +33,7 @@ func BaseContext(platformInterface PlatformInterface) context.Context {
}) })
} }
} }
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry) return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
} }
func parseConfig(ctx context.Context, configContent string) (option.Options, error) { func parseConfig(ctx context.Context, configContent string) (option.Options, error) {

4
go.mod
View File

@ -6,9 +6,11 @@ require (
github.com/anytls/sing-anytls v0.0.8 github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0 github.com/caddyserver/certmagic v0.23.0
github.com/cloudflare/circl v1.6.1 github.com/cloudflare/circl v1.6.1
github.com/coder/websocket v1.8.12
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2 github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/libdns/alidns v1.0.4-libdns.v1.beta1 github.com/libdns/alidns v1.0.4-libdns.v1.beta1
@ -65,7 +67,6 @@ require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
@ -78,7 +79,6 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect

View File

@ -3,10 +3,12 @@ package include
import ( import (
"context" "context"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport" "github.com/sagernet/sing-box/dns/transport"
@ -33,9 +35,15 @@ import (
"github.com/sagernet/sing-box/protocol/tun" "github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless" "github.com/sagernet/sing-box/protocol/vless"
"github.com/sagernet/sing-box/protocol/vmess" "github.com/sagernet/sing-box/protocol/vmess"
"github.com/sagernet/sing-box/service/resolved"
"github.com/sagernet/sing-box/service/ssmapi"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
) )
func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
}
func InboundRegistry() *inbound.Registry { func InboundRegistry() *inbound.Registry {
registry := inbound.NewRegistry() registry := inbound.NewRegistry()
@ -110,6 +118,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
hosts.RegisterTransport(registry) hosts.RegisterTransport(registry)
local.RegisterTransport(registry) local.RegisterTransport(registry)
fakeip.RegisterTransport(registry) fakeip.RegisterTransport(registry)
resolved.RegisterTransport(registry)
registerQUICTransports(registry) registerQUICTransports(registry)
registerDHCPTransport(registry) registerDHCPTransport(registry)
@ -118,6 +127,17 @@ func DNSTransportRegistry() *dns.TransportRegistry {
return registry return registry
} }
func ServiceRegistry() *service.Registry {
registry := service.NewRegistry()
resolved.RegisterService(registry)
ssmapi.RegisterService(registry)
registerDERPService(registry)
return registry
}
func registerStubForRemovedInbounds(registry *inbound.Registry) { func registerStubForRemovedInbounds(registry *inbound.Registry) {
inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

@ -4,8 +4,10 @@ package include
import ( import (
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/protocol/tailscale" "github.com/sagernet/sing-box/protocol/tailscale"
"github.com/sagernet/sing-box/service/derp"
) )
func registerTailscaleEndpoint(registry *endpoint.Registry) { func registerTailscaleEndpoint(registry *endpoint.Registry) {
@ -15,3 +17,7 @@ func registerTailscaleEndpoint(registry *endpoint.Registry) {
func registerTailscaleTransport(registry *dns.TransportRegistry) { func registerTailscaleTransport(registry *dns.TransportRegistry) {
tailscale.RegistryTransport(registry) tailscale.RegistryTransport(registry)
} }
func registerDERPService(registry *service.Registry) {
derp.Register(registry)
}

View File

@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns" "github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
@ -25,3 +26,9 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`) return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
}) })
} }
func registerDERPService(registry *service.Registry) {
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
})
}

View File

@ -94,6 +94,7 @@ nav:
- DHCP: configuration/dns/server/dhcp.md - DHCP: configuration/dns/server/dhcp.md
- FakeIP: configuration/dns/server/fakeip.md - FakeIP: configuration/dns/server/fakeip.md
- Tailscale: configuration/dns/server/tailscale.md - Tailscale: configuration/dns/server/tailscale.md
- Resolved: configuration/dns/server/resolved.md
- DNS Rule: configuration/dns/rule.md - DNS Rule: configuration/dns/rule.md
- DNS Rule Action: configuration/dns/rule_action.md - DNS Rule Action: configuration/dns/rule_action.md
- FakeIP: configuration/dns/fakeip.md - FakeIP: configuration/dns/fakeip.md
@ -169,6 +170,11 @@ nav:
- DNS: configuration/outbound/dns.md - DNS: configuration/outbound/dns.md
- Selector: configuration/outbound/selector.md - Selector: configuration/outbound/selector.md
- URLTest: configuration/outbound/urltest.md - URLTest: configuration/outbound/urltest.md
- Service:
- configuration/service/index.md
- DERP: configuration/service/derp.md
- Resolved: configuration/service/resolved.md
- SSM API: configuration/service/ssm-api.md
markdown_extensions: markdown_extensions:
- pymdownx.inlinehilite - pymdownx.inlinehilite
- pymdownx.snippets - pymdownx.snippets

View File

@ -121,7 +121,6 @@ type LegacyDNSFakeIPOptions struct {
type DNSTransportOptionsRegistry interface { type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool) CreateOptions(transportType string) (any, bool)
} }
type _DNSServerOptions struct { type _DNSServerOptions struct {
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Tag string `json:"tag,omitempty"` Tag string `json:"tag,omitempty"`

View File

@ -32,11 +32,11 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
} }
registry := service.FromContext[EndpointOptionsRegistry](ctx) registry := service.FromContext[EndpointOptionsRegistry](ctx)
if registry == nil { if registry == nil {
return E.New("missing Endpoint fields registry in context") return E.New("missing endpoint fields registry in context")
} }
options, loaded := registry.CreateOptions(h.Type) options, loaded := registry.CreateOptions(h.Type)
if !loaded { if !loaded {
return E.New("unknown inbound type: ", h.Type) return E.New("unknown endpoint type: ", h.Type)
} }
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options) err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
if err != nil { if err != nil {

View File

@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
} }
registry := service.FromContext[InboundOptionsRegistry](ctx) registry := service.FromContext[InboundOptionsRegistry](ctx)
if registry == nil { if registry == nil {
return E.New("missing Inbound fields registry in context") return E.New("missing inbound fields registry in context")
} }
options, loaded := registry.CreateOptions(h.Type) options, loaded := registry.CreateOptions(h.Type)
if !loaded { if !loaded {

View File

@ -19,6 +19,7 @@ type _Options struct {
Inbounds []Inbound `json:"inbounds,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"`
Route *RouteOptions `json:"route,omitempty"` Route *RouteOptions `json:"route,omitempty"`
Services []Service `json:"services,omitempty"`
Experimental *ExperimentalOptions `json:"experimental,omitempty"` Experimental *ExperimentalOptions `json:"experimental,omitempty"`
} }

49
option/resolved.go Normal file
View File

@ -0,0 +1,49 @@
package option
import (
"context"
"net/netip"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
)
type _ResolvedServiceOptions struct {
ListenOptions
}
type ResolvedServiceOptions _ResolvedServiceOptions
func (r ResolvedServiceOptions) MarshalJSONContext(ctx context.Context) ([]byte, error) {
if r.Listen != nil && netip.Addr(*r.Listen) == (netip.AddrFrom4([4]byte{127, 0, 0, 53})) {
r.Listen = nil
}
if r.ListenPort == 53 {
r.ListenPort = 0
}
return json.MarshalContext(ctx, (*_ResolvedServiceOptions)(&r))
}
func (r *ResolvedServiceOptions) UnmarshalJSONContext(ctx context.Context, bytes []byte) error {
err := json.UnmarshalContextDisallowUnknownFields(ctx, bytes, (*_ResolvedServiceOptions)(r))
if err != nil {
return err
}
if r.Listen == nil {
r.Listen = (*badoption.Addr)(common.Ptr(netip.AddrFrom4([4]byte{127, 0, 0, 53})))
}
if r.ListenPort == 0 {
r.ListenPort = 53
}
return nil
}
type ResolvedDNSServerOptions struct {
Service string `json:"service"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
// NDots int `json:"ndots,omitempty"`
// Timeout badoption.Duration `json:"timeout,omitempty"`
// Attempts int `json:"attempts,omitempty"`
// Rotate bool `json:"rotate,omitempty"`
}

View File

@ -158,6 +158,7 @@ type RawRouteOptionsActionOptions struct {
TLSFragment bool `json:"tls_fragment,omitempty"` TLSFragment bool `json:"tls_fragment,omitempty"`
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"` TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
TLSRecordFragment bool `json:"tls_record_fragment,omitempty"`
} }
type RouteOptionsActionOptions RawRouteOptionsActionOptions type RouteOptionsActionOptions RawRouteOptionsActionOptions
@ -170,6 +171,9 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error {
if *r == (RouteOptionsActionOptions{}) { if *r == (RouteOptionsActionOptions{}) {
return E.New("empty route option action") return E.New("empty route option action")
} }
if r.TLSFragment && r.TLSRecordFragment {
return E.New("`tls_fragment` and `tls_record_fragment` are mutually exclusive")
}
return nil return nil
} }

47
option/service.go Normal file
View File

@ -0,0 +1,47 @@
package option
import (
"context"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badjson"
"github.com/sagernet/sing/service"
)
type ServiceOptionsRegistry interface {
CreateOptions(serviceType string) (any, bool)
}
type _Service struct {
Type string `json:"type"`
Tag string `json:"tag,omitempty"`
Options any `json:"-"`
}
type Service _Service
func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) {
return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options)
}
func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error {
err := json.UnmarshalContext(ctx, content, (*_Service)(h))
if err != nil {
return err
}
registry := service.FromContext[ServiceOptionsRegistry](ctx)
if registry == nil {
return E.New("missing service fields registry in context")
}
options, loaded := registry.CreateOptions(h.Type)
if !loaded {
return E.New("unknown inbound type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options)
if err != nil {
return err
}
h.Options = options
return nil
}

View File

@ -8,6 +8,7 @@ type ShadowsocksInboundOptions struct {
Users []ShadowsocksUser `json:"users,omitempty"` Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"` Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"` Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
Managed bool `json:"managed,omitempty"`
} }
type ShadowsocksUser struct { type ShadowsocksUser struct {

11
option/ssmapi.go Normal file
View File

@ -0,0 +1,11 @@
package option
import (
"github.com/sagernet/sing/common/json/badjson"
)
type SSMAPIServiceOptions struct {
ListenOptions
Servers *badjson.TypedMap[string, string] `json:"servers"`
InboundTLSOptionsContainer
}

View File

@ -2,6 +2,12 @@ package option
import ( import (
"net/netip" "net/netip"
"net/url"
"reflect"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
) )
type TailscaleEndpointOptions struct { type TailscaleEndpointOptions struct {
@ -11,6 +17,7 @@ type TailscaleEndpointOptions struct {
ControlURL string `json:"control_url,omitempty"` ControlURL string `json:"control_url,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"` Ephemeral bool `json:"ephemeral,omitempty"`
Hostname string `json:"hostname,omitempty"` Hostname string `json:"hostname,omitempty"`
AcceptRoutes bool `json:"accept_routes,omitempty"`
ExitNode string `json:"exit_node,omitempty"` ExitNode string `json:"exit_node,omitempty"`
ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"` ExitNodeAllowLANAccess bool `json:"exit_node_allow_lan_access,omitempty"`
AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"` AdvertiseRoutes []netip.Prefix `json:"advertise_routes,omitempty"`
@ -22,3 +29,88 @@ type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,omitempty"` Endpoint string `json:"endpoint,omitempty"`
AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"` AcceptDefaultResolvers bool `json:"accept_default_resolvers,omitempty"`
} }
type DERPServiceOptions struct {
ListenOptions
InboundTLSOptionsContainer
ConfigPath string `json:"config_path,omitempty"`
VerifyClientEndpoint badoption.Listable[string] `json:"verify_client_endpoint,omitempty"`
VerifyClientURL badoption.Listable[*DERPVerifyClientURLOptions] `json:"verify_client_url,omitempty"`
Home string `json:"home,omitempty"`
MeshWith badoption.Listable[*DERPMeshOptions] `json:"mesh_with,omitempty"`
MeshPSK string `json:"mesh_psk,omitempty"`
MeshPSKFile string `json:"mesh_psk_file,omitempty"`
STUN *DERPSTUNListenOptions `json:"stun,omitempty"`
}
type _DERPVerifyClientURLOptions struct {
URL string `json:"url,omitempty"`
DialerOptions
}
type DERPVerifyClientURLOptions _DERPVerifyClientURLOptions
func (d DERPVerifyClientURLOptions) ServerIsDomain() bool {
verifyURL, err := url.Parse(d.URL)
if err != nil {
return false
}
return M.IsDomainName(verifyURL.Host)
}
func (d DERPVerifyClientURLOptions) MarshalJSON() ([]byte, error) {
if reflect.DeepEqual(d, _DERPVerifyClientURLOptions{}) {
return json.Marshal(d.URL)
} else {
return json.Marshal(_DERPVerifyClientURLOptions(d))
}
}
func (d *DERPVerifyClientURLOptions) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err == nil {
d.URL = stringValue
return nil
}
return json.Unmarshal(bytes, (*_DERPVerifyClientURLOptions)(d))
}
type DERPMeshOptions struct {
ServerOptions
Host string `json:"host,omitempty"`
OutboundTLSOptionsContainer
DialerOptions
}
type _DERPSTUNListenOptions struct {
Enabled bool
ListenOptions
}
type DERPSTUNListenOptions _DERPSTUNListenOptions
func (d DERPSTUNListenOptions) MarshalJSON() ([]byte, error) {
portOptions := _DERPSTUNListenOptions{
Enabled: d.Enabled,
ListenOptions: ListenOptions{
ListenPort: d.ListenPort,
},
}
if _DERPSTUNListenOptions(d) == portOptions {
return json.Marshal(d.Enabled)
} else {
return json.Marshal(_DERPSTUNListenOptions(d))
}
}
func (d *DERPSTUNListenOptions) UnmarshalJSON(bytes []byte) error {
var portValue uint16
err := json.Unmarshal(bytes, &portValue)
if err == nil {
d.Enabled = true
d.ListenPort = portValue
return nil
}
return json.Unmarshal(bytes, (*_DERPSTUNListenOptions)(d))
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"net" "net"
"net/netip" "net/netip"
"syscall"
"time" "time"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -17,7 +16,6 @@ import (
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/udpnat2" "github.com/sagernet/sing/common/udpnat2"
@ -57,6 +55,7 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
Listen: options.ListenOptions, Listen: options.ListenOptions,
ConnectionHandler: tproxy, ConnectionHandler: tproxy,
OOBPacketHandler: tproxy, OOBPacketHandler: tproxy,
TProxy: true,
}) })
return tproxy, nil return tproxy, nil
} }
@ -65,27 +64,7 @@ func (t *TProxy) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
err := t.listener.Start() return t.listener.Start()
if err != nil {
return err
}
if listener := t.listener.TCPListener(); listener != nil {
err = control.Conn(common.MustCast[syscall.Conn](listener), func(fd uintptr) error {
return redir.TProxy(fd, M.SocksaddrFromNet(listener.Addr()).Addr.Is6())
})
if err != nil {
return E.Cause(err, "configure tproxy TCP listener")
}
}
if conn := t.listener.UDPConn(); conn != nil {
err = control.Conn(conn, func(fd uintptr) error {
return redir.TProxy(fd, M.SocksaddrFromNet(conn.LocalAddr()).Addr.Is6())
})
if err != nil {
return E.Cause(err, "configure tproxy UDP listener")
}
}
return nil
} }
func (t *TProxy) Close() error { func (t *TProxy) Close() error {
@ -154,10 +133,10 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks
return err return err
} }
} }
var listener net.ListenConfig var listenConfig net.ListenConfig
listener.Control = control.Append(listener.Control, control.ReuseAddr()) listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
listener.Control = control.Append(listener.Control, redir.TProxyWriteBack()) listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack())
packetConn, err := w.listener.ListenPacket(listener, w.ctx, "udp", destination.String()) packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String())
if err != nil { if err != nil {
return err return err
} }

View File

@ -32,8 +32,10 @@ func RegisterInbound(registry *inbound.Registry) {
func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
if len(options.Users) > 0 && len(options.Destinations) > 0 { if len(options.Users) > 0 && len(options.Destinations) > 0 {
return nil, E.New("users and destinations options must not be combined") return nil, E.New("users and destinations options must not be combined")
} else if options.Managed && (len(options.Users) > 0 || len(options.Destinations) > 0) {
return nil, E.New("users and destinations options are not supported in managed servers")
} }
if len(options.Users) > 0 { if len(options.Users) > 0 || options.Managed {
return newMultiInbound(ctx, router, logger, tag, options) return newMultiInbound(ctx, router, logger, tag, options)
} else if len(options.Destinations) > 0 { } else if len(options.Destinations) > 0 {
return newRelayInbound(ctx, router, logger, tag, options) return newRelayInbound(ctx, router, logger, tag, options)

View File

@ -28,7 +28,10 @@ import (
"github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/ntp"
) )
var _ adapter.TCPInjectableInbound = (*MultiInbound)(nil) var (
_ adapter.TCPInjectableInbound = (*MultiInbound)(nil)
_ adapter.ManagedSSMServer = (*MultiInbound)(nil)
)
type MultiInbound struct { type MultiInbound struct {
inbound.Adapter inbound.Adapter
@ -38,6 +41,7 @@ type MultiInbound struct {
listener *listener.Listener listener *listener.Listener
service shadowsocks.MultiService[int] service shadowsocks.MultiService[int]
users []option.ShadowsocksUser users []option.ShadowsocksUser
tracker adapter.SSMTracker
} }
func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) { func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) {
@ -79,13 +83,15 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int { if len(options.Users) > 0 {
return index err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
}), common.Map(options.Users, func(user option.ShadowsocksUser) string { return index
return user.Password }), common.Map(options.Users, func(user option.ShadowsocksUser) string {
})) return user.Password
if err != nil { }))
return nil, err if err != nil {
return nil, err
}
} }
inbound.service = service inbound.service = service
inbound.users = options.Users inbound.users = options.Users
@ -112,6 +118,25 @@ func (h *MultiInbound) Close() error {
return h.listener.Close() return h.listener.Close()
} }
func (h *MultiInbound) SetTracker(tracker adapter.SSMTracker) {
h.tracker = tracker
}
func (h *MultiInbound) UpdateUsers(users []string, uPSKs []string) error {
err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
return index
}), uPSKs)
if err != nil {
return err
}
h.users = common.Map(users, func(user string) option.ShadowsocksUser {
return option.ShadowsocksUser{
Name: user,
}
})
return nil
}
//nolint:staticcheck //nolint:staticcheck
func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata))
@ -151,6 +176,9 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat
metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck //nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
if h.tracker != nil {
conn = h.tracker.TrackConnection(conn, metadata)
}
return h.router.RouteConnection(ctx, conn, metadata) return h.router.RouteConnection(ctx, conn, metadata)
} }
@ -174,6 +202,9 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon
metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundDetour = h.listener.ListenOptions().Detour
//nolint:staticcheck //nolint:staticcheck
metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.InboundOptions = h.listener.ListenOptions().InboundOptions
if h.tracker != nil {
conn = h.tracker.TrackPacketConnection(conn, metadata)
}
return h.router.RoutePacketConnection(ctx, conn, metadata) return h.router.RoutePacketConnection(ctx, conn, metadata)
} }

View File

@ -72,6 +72,7 @@ type Endpoint struct {
filter *atomic.Pointer[filter.Filter] filter *atomic.Pointer[filter.Filter]
onReconfig wgengine.ReconfigListener onReconfig wgengine.ReconfigListener
acceptRoutes bool
exitNode string exitNode string
exitNodeAllowLANAccess bool exitNodeAllowLANAccess bool
advertiseRoutes []netip.Prefix advertiseRoutes []netip.Prefix
@ -170,6 +171,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
network: service.FromContext[adapter.NetworkManager](ctx), network: service.FromContext[adapter.NetworkManager](ctx),
platformInterface: service.FromContext[platform.Interface](ctx), platformInterface: service.FromContext[platform.Interface](ctx),
server: server, server: server,
acceptRoutes: options.AcceptRoutes,
exitNode: options.ExitNode, exitNode: options.ExitNode,
exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess, exitNodeAllowLANAccess: options.ExitNodeAllowLANAccess,
advertiseRoutes: options.AdvertiseRoutes, advertiseRoutes: options.AdvertiseRoutes,
@ -219,6 +221,14 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
} }
ipStack := t.server.ExportNetstack().ExportIPStack() ipStack := t.server.ExportNetstack().ExportIPStack()
gErr := ipStack.SetSpoofing(tun.DefaultNIC, true)
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
gErr = ipStack.SetPromiscuousMode(tun.DefaultNIC, true)
if gErr != nil {
return gonet.TranslateNetstackError(gErr)
}
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket) ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(t.ctx, ipStack, t).HandlePacket)
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout) udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket) ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
@ -226,6 +236,10 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
localBackend := t.server.ExportLocalBackend() localBackend := t.server.ExportLocalBackend()
perfs := &ipn.MaskedPrefs{ perfs := &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
RouteAll: t.acceptRoutes,
},
RouteAllSet: true,
ExitNodeIPSet: true, ExitNodeIPSet: true,
AdvertiseRoutesSet: true, AdvertiseRoutesSet: true,
} }

View File

@ -0,0 +1,15 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.freedesktop.resolve1"/>
<allow send_destination="org.freedesktop.resolve1"/>
<allow receive_sender="org.freedesktop.resolve1"/>
</policy>
<policy user="sing-box">
<allow own="org.freedesktop.resolve1"/>
<allow send_destination="org.freedesktop.resolve1"/>
<allow receive_sender="org.freedesktop.resolve1"/>
</policy>
</busconfig>

View File

@ -0,0 +1,3 @@
#!/bin/sh
systemd-sysusers sing-box.conf

View File

@ -0,0 +1,8 @@
polkit.addRule(function(action, subject) {
if ((action.id == "org.freedesktop.resolve1.set-domains" ||
action.id == "org.freedesktop.resolve1.set-default-route" ||
action.id == "org.freedesktop.resolve1.set-dns-servers") &&
subject.user == "sing-box") {
return polkit.Result.YES;
}
});

View File

@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target After=network.target nss-lookup.target network-online.target
[Service] [Service]
User=sing-box
StateDirectory=sing-box
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run ExecStart=/usr/bin/sing-box -D /var/lib/sing-box -C /etc/sing-box run

View File

@ -0,0 +1 @@
u sing-box - "sing-box Service"

View File

@ -4,6 +4,8 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target After=network.target nss-lookup.target network-online.target
[Service] [Service]
User=sing-box
StateDirectory=sing-box-%i
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run ExecStart=/usr/bin/sing-box -D /var/lib/sing-box-%i -c /etc/sing-box/%i.json run

View File

@ -95,15 +95,9 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
if fallbackDelay == 0 { if fallbackDelay == 0 {
fallbackDelay = C.TLSFragmentFallbackDelay fallbackDelay = C.TLSFragmentFallbackDelay
} }
var newConn *tf.Conn remoteConn = tf.NewConn(remoteConn, ctx, false, fallbackDelay)
newConn, err = tf.NewConn(remoteConn, ctx, fallbackDelay) } else if metadata.TLSRecordFragment {
if err != nil { remoteConn = tf.NewConn(remoteConn, ctx, true, 0)
conn.Close()
remoteConn.Close()
m.logger.ErrorContext(ctx, err)
return
}
remoteConn = newConn
} }
m.access.Lock() m.access.Lock()
element := m.connections.PushBack(conn) element := m.connections.PushBack(conn)

View File

@ -27,12 +27,16 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata) err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
if err != nil { if err != nil {
return err if !E.IsClosedOrCanceled(err) {
return err
} else {
return nil
}
} }
} }
} }
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
for _, packet := range packetBuffers { for _, packet := range packetBuffers {
@ -49,19 +53,20 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
metadata: metadata, metadata: metadata,
onClose: onClose, onClose: onClose,
}) })
return return nil
} }
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata) err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err) N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) { if err != nil && !E.IsClosedOrCanceled(err) {
r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection")) return E.Cause(err, "process DNS packet")
} }
return nil
} }
func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) { func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination) err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) { if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection")) logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
} }
} }

View File

@ -57,6 +57,15 @@ type NetworkManager struct {
func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) { func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) {
defaultDomainResolver := common.PtrValueOrDefault(routeOptions.DefaultDomainResolver) defaultDomainResolver := common.PtrValueOrDefault(routeOptions.DefaultDomainResolver)
if routeOptions.AutoDetectInterface && !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`auto_detect_interface` is only supported on Linux, Windows and macOS")
} else if routeOptions.OverrideAndroidVPN && !C.IsAndroid {
return nil, E.New("`override_android_vpn` is only supported on Android")
} else if routeOptions.DefaultInterface != "" && !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`default_interface` is only supported on Linux, Windows and macOS")
} else if routeOptions.DefaultMark != 0 && !C.IsLinux {
return nil, E.New("`default_mark` is only supported on linux")
}
nm := &NetworkManager{ nm := &NetworkManager{
logger: logger, logger: logger,
interfaceFinder: control.NewDefaultInterfaceFinder(), interfaceFinder: control.NewDefaultInterfaceFinder(),

View File

@ -6,7 +6,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"os/user"
"strings" "strings"
"time" "time"
@ -113,8 +112,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
} }
case *rule.RuleActionReject: case *rule.RuleActionReject:
buf.ReleaseMulti(buffers) buf.ReleaseMulti(buffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
for _, buffer := range buffers { for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer) conn = bufio.NewCachedConn(conn, buffer)
@ -229,11 +227,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
} }
case *rule.RuleActionReject: case *rule.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose) return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
return nil
} }
} }
if selectedRule == nil || selectReturn { if selectedRule == nil || selectReturn {
@ -296,16 +292,16 @@ func (r *Router) matchRule(
r.logger.InfoContext(ctx, "failed to search process: ", fErr) r.logger.InfoContext(ctx, "failed to search process: ", fErr)
} else { } else {
if processInfo.ProcessPath != "" { if processInfo.ProcessPath != "" {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) if processInfo.User != "" {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User)
} else if processInfo.UserId != -1 {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
} else {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
}
} else if processInfo.PackageName != "" { } else if processInfo.PackageName != "" {
r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName) r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
} else if processInfo.UserId != -1 { } else if processInfo.UserId != -1 {
if /*needUserName &&*/ true {
osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
if osUser != nil {
processInfo.User = osUser.Username
}
}
if processInfo.User != "" { if processInfo.User != "" {
r.logger.InfoContext(ctx, "found user: ", processInfo.User) r.logger.InfoContext(ctx, "found user: ", processInfo.User)
} else { } else {
@ -450,6 +446,9 @@ match:
metadata.TLSFragment = true metadata.TLSFragment = true
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
} }
if routeOptions.TLSRecordFragment {
metadata.TLSRecordFragment = true
}
} }
switch action := currentRule.Action().(type) { switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff: case *rule.RuleActionSniff:

View File

@ -40,6 +40,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
UDPConnect: action.RouteOptions.UDPConnect, UDPConnect: action.RouteOptions.UDPConnect,
TLSFragment: action.RouteOptions.TLSFragment, TLSFragment: action.RouteOptions.TLSFragment,
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay), TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
TLSRecordFragment: action.RouteOptions.TLSRecordFragment,
}, },
}, nil }, nil
case C.RuleActionTypeRouteOptions: case C.RuleActionTypeRouteOptions:
@ -53,6 +54,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout), UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
TLSFragment: action.RouteOptionsOptions.TLSFragment, TLSFragment: action.RouteOptionsOptions.TLSFragment,
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay), TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
TLSRecordFragment: action.RouteOptionsOptions.TLSRecordFragment,
}, nil }, nil
case C.RuleActionTypeDirect: case C.RuleActionTypeDirect:
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false) directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
@ -152,15 +154,7 @@ func (r *RuleActionRoute) Type() string {
func (r *RuleActionRoute) String() string { func (r *RuleActionRoute) String() string {
var descriptions []string var descriptions []string
descriptions = append(descriptions, r.Outbound) descriptions = append(descriptions, r.Outbound)
if r.UDPDisableDomainUnmapping { descriptions = append(descriptions, r.Descriptions()...)
descriptions = append(descriptions, "udp-disable-domain-unmapping")
}
if r.UDPConnect {
descriptions = append(descriptions, "udp-connect")
}
if r.TLSFragment {
descriptions = append(descriptions, "tls-fragment")
}
return F.ToString("route(", strings.Join(descriptions, ","), ")") return F.ToString("route(", strings.Join(descriptions, ","), ")")
} }
@ -176,6 +170,7 @@ type RuleActionRouteOptions struct {
UDPTimeout time.Duration UDPTimeout time.Duration
TLSFragment bool TLSFragment bool
TLSFragmentFallbackDelay time.Duration TLSFragmentFallbackDelay time.Duration
TLSRecordFragment bool
} }
func (r *RuleActionRouteOptions) Type() string { func (r *RuleActionRouteOptions) Type() string {
@ -183,6 +178,10 @@ func (r *RuleActionRouteOptions) Type() string {
} }
func (r *RuleActionRouteOptions) String() string { func (r *RuleActionRouteOptions) String() string {
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
}
func (r *RuleActionRouteOptions) Descriptions() []string {
var descriptions []string var descriptions []string
if r.OverrideAddress.IsValid() { if r.OverrideAddress.IsValid() {
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString())) descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
@ -211,7 +210,16 @@ func (r *RuleActionRouteOptions) String() string {
if r.UDPTimeout > 0 { if r.UDPTimeout > 0 {
descriptions = append(descriptions, "udp-timeout") descriptions = append(descriptions, "udp-timeout")
} }
return F.ToString("route-options(", strings.Join(descriptions, ","), ")") if r.TLSFragment {
descriptions = append(descriptions, "tls-fragment")
}
if r.TLSFragmentFallbackDelay > 0 {
descriptions = append(descriptions, F.ToString("tls-fragment-fallback-delay=", r.TLSFragmentFallbackDelay.String()))
}
if r.TLSRecordFragment {
descriptions = append(descriptions, "tls-record-fragment")
}
return descriptions
} }
type RuleActionDNSRoute struct { type RuleActionDNSRoute struct {

519
service/derp/service.go Normal file
View File

@ -0,0 +1,519 @@
package derp
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
boxScale "github.com/sagernet/sing-box/protocol/tailscale"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json/badoption"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/tailscale/client/tailscale"
"github.com/sagernet/tailscale/derp"
"github.com/sagernet/tailscale/derp/derphttp"
"github.com/sagernet/tailscale/net/netmon"
"github.com/sagernet/tailscale/net/stun"
"github.com/sagernet/tailscale/net/wsconn"
"github.com/sagernet/tailscale/tsweb"
"github.com/sagernet/tailscale/types/key"
"github.com/coder/websocket"
"github.com/go-chi/render"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func Register(registry *boxService.Registry) {
boxService.Register[option.DERPServiceOptions](registry, C.TypeDERP, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger logger.ContextLogger
listener *listener.Listener
stunListener *listener.Listener
tlsConfig tls.ServerConfig
server *derp.Server
configPath string
verifyClientEndpoint []string
verifyClientURL []*option.DERPVerifyClientURLOptions
home string
meshKey string
meshKeyPath string
meshWith []*option.DERPMeshOptions
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
if options.TLS == nil || !options.TLS.Enabled {
return nil, E.New("TLS is required for DERP server")
}
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
var configPath string
if options.ConfigPath != "" {
configPath = filemanager.BasePath(ctx, os.ExpandEnv(options.ConfigPath))
} else {
return nil, E.New("missing config_path")
}
if options.MeshPSK != "" {
err = checkMeshKey(options.MeshPSK)
if err != nil {
return nil, E.Cause(err, "invalid mesh_psk")
}
}
var stunListener *listener.Listener
if options.STUN != nil && options.STUN.Enabled {
if options.STUN.Listen == nil {
options.STUN.Listen = (*badoption.Addr)(common.Ptr(netip.IPv6Unspecified()))
}
if options.STUN.ListenPort == 0 {
options.STUN.ListenPort = 3478
}
stunListener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkUDP},
Listen: options.STUN.ListenOptions,
})
}
return &Service{
Adapter: boxService.NewAdapter(C.TypeDERP, tag),
ctx: ctx,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
stunListener: stunListener,
tlsConfig: tlsConfig,
configPath: configPath,
verifyClientEndpoint: options.VerifyClientEndpoint,
verifyClientURL: options.VerifyClientURL,
home: options.Home,
meshKey: options.MeshPSK,
meshKeyPath: options.MeshPSKFile,
meshWith: options.MeshWith,
}, nil
}
func (d *Service) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateStart:
config, err := readDERPConfig(d.configPath)
if err != nil {
return err
}
server := derp.NewServer(config.PrivateKey, func(format string, args ...any) {
d.logger.Debug(fmt.Sprintf(format, args...))
})
if len(d.verifyClientURL) > 0 {
var httpClients []*http.Client
var urls []string
for index, options := range d.verifyClientURL {
verifyDialer, createErr := dialer.NewWithOptions(dialer.Options{
Context: d.ctx,
Options: options.DialerOptions,
RemoteIsDomain: options.ServerIsDomain(),
NewDialer: true,
})
if createErr != nil {
return E.Cause(createErr, "verify_client_url[", index, "]")
}
httpClients = append(httpClients, &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
})
urls = append(urls, options.URL)
}
server.SetVerifyClientHTTPClient(httpClients)
server.SetVerifyClientURL(urls)
}
if d.meshKey != "" {
server.SetMeshKey(d.meshKey)
} else if d.meshKeyPath != "" {
var meshKeyContent []byte
meshKeyContent, err = os.ReadFile(d.meshKeyPath)
if err != nil {
return err
}
err = checkMeshKey(string(meshKeyContent))
if err != nil {
return E.Cause(err, "invalid mesh_psk_path file")
}
server.SetMeshKey(string(meshKeyContent))
}
d.server = server
derpMux := http.NewServeMux()
derpHandler := derphttp.Handler(server)
derpHandler = addWebSocketSupport(server, derpHandler)
derpMux.Handle("/derp", derpHandler)
homeHandler, ok := getHomeHandler(d.home)
if !ok {
return E.New("invalid home value: ", d.home)
}
derpMux.HandleFunc("/derp/probe", derphttp.ProbeHandler)
derpMux.HandleFunc("/derp/latency-check", derphttp.ProbeHandler)
derpMux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS(d.ctx)))
derpMux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
homeHandler.ServeHTTP(w, r)
}))
derpMux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
io.WriteString(w, "User-agent: *\nDisallow: /\n")
}))
derpMux.Handle("/generate_204", http.HandlerFunc(derphttp.ServeNoContent))
err = d.tlsConfig.Start()
if err != nil {
return err
}
tcpListener, err := d.listener.ListenTCP()
if err != nil {
return err
}
if len(d.tlsConfig.NextProtos()) == 0 {
d.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
} else if !common.Contains(d.tlsConfig.NextProtos(), http2.NextProtoTLS) {
d.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, d.tlsConfig.NextProtos()...))
}
tcpListener = aTLS.NewListener(tcpListener, d.tlsConfig)
httpServer := &http.Server{
Handler: h2c.NewHandler(derpMux, &http2.Server{}),
}
go httpServer.Serve(tcpListener)
if d.stunListener != nil {
stunConn, err := d.stunListener.ListenUDP()
if err != nil {
return err
}
go d.loopSTUNPacket(stunConn.(*net.UDPConn))
}
case adapter.StartStatePostStart:
if len(d.verifyClientEndpoint) > 0 {
var endpoints []*tailscale.LocalClient
endpointManager := service.FromContext[adapter.EndpointManager](d.ctx)
for _, endpointTag := range d.verifyClientEndpoint {
endpoint, loaded := endpointManager.Get(endpointTag)
if !loaded {
return E.New("verify_client_endpoint: endpoint not found: ", endpointTag)
}
tsEndpoint, isTailscale := endpoint.(*boxScale.Endpoint)
if !isTailscale {
return E.New("verify_client_endpoint: endpoint is not Tailscale: ", endpointTag)
}
localClient, err := tsEndpoint.Server().LocalClient()
if err != nil {
return err
}
endpoints = append(endpoints, localClient)
}
d.server.SetVerifyClientLocalClient(endpoints)
}
if len(d.meshWith) > 0 {
if !d.server.HasMeshKey() {
return E.New("missing mesh psk")
}
for _, options := range d.meshWith {
err := d.startMeshWithHost(d.server, options)
if err != nil {
return err
}
}
}
}
return nil
}
func checkMeshKey(meshKey string) error {
checkRegex, err := regexp.Compile(`^[0-9a-f]{64}$`)
if err != nil {
return err
}
if !checkRegex.MatchString(meshKey) {
return E.New("key must contain exactly 64 hex digits")
}
return nil
}
func (d *Service) startMeshWithHost(derpServer *derp.Server, server *option.DERPMeshOptions) error {
meshDialer, err := dialer.NewWithOptions(dialer.Options{
Context: d.ctx,
Options: server.DialerOptions,
RemoteIsDomain: server.ServerIsDomain(),
NewDialer: true,
})
if err != nil {
return err
}
var hostname string
if server.Host != "" {
hostname = server.Host
} else {
hostname = server.Server
}
var stdConfig *tls.STDConfig
if server.TLS != nil && server.TLS.Enabled {
tlsConfig, err := tls.NewClient(d.ctx, hostname, common.PtrValueOrDefault(server.TLS))
if err != nil {
return err
}
stdConfig, err = tlsConfig.Config()
if err != nil {
return err
}
}
logf := func(format string, args ...any) {
d.logger.Debug(F.ToString("mesh(", hostname, "): ", fmt.Sprintf(format, args...)))
}
var meshHost string
if server.ServerPort == 0 || server.ServerPort == 443 {
meshHost = hostname
} else {
meshHost = M.ParseSocksaddrHostPort(hostname, server.ServerPort).String()
}
var serverURL string
if stdConfig != nil {
serverURL = "https://" + meshHost + "/derp"
} else {
serverURL = "http://" + meshHost + "/derp"
}
meshClient, err := derphttp.NewClient(derpServer.PrivateKey(), serverURL, logf, netmon.NewStatic())
if err != nil {
return err
}
meshClient.TLSConfig = stdConfig
meshClient.MeshKey = derpServer.MeshKey()
meshClient.WatchConnectionChanges = true
meshClient.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
return meshDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
})
add := func(m derp.PeerPresentMessage) { derpServer.AddPacketForwarder(m.Key, meshClient) }
remove := func(m derp.PeerGoneMessage) { derpServer.RemovePacketForwarder(m.Peer, meshClient) }
go meshClient.RunWatchConnectionLoop(context.Background(), derpServer.PublicKey(), logf, add, remove)
return nil
}
func (d *Service) Close() error {
return common.Close(
common.PtrOrNil(d.listener),
d.tlsConfig,
)
}
var homePage = `
<h1>DERP</h1>
<p>
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
</p>
<p>
It provides STUN, interactive connectivity establishment, and relaying of end-to-end encrypted traffic
for Tailscale clients.
</p>
<p>
Documentation:
</p>
<ul>
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
</body>
</html>
`
func getHomeHandler(val string) (_ http.Handler, ok bool) {
if val == "" {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
w.Write([]byte(homePage))
}), true
}
if val == "blank" {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
}), true
}
if strings.HasPrefix(val, "http://") || strings.HasPrefix(val, "https://") {
return http.RedirectHandler(val, http.StatusFound), true
}
return nil, false
}
func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
up := strings.ToLower(r.Header.Get("Upgrade"))
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
base.ServeHTTP(w, r)
return
}
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because we transmit WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return
}
defer c.Close(websocket.StatusInternalError, "closing")
if c.Subprotocol() != "derp" {
c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
return
}
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary, r.RemoteAddr)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
})
}
func handleBootstrapDNS(ctx context.Context) http.HandlerFunc {
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Connection", "close")
if queryDomain := r.URL.Query().Get("q"); queryDomain != "" {
addresses, err := dnsRouter.Lookup(ctx, queryDomain, adapter.DNSQueryOptions{})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
render.JSON(w, r, render.M{
queryDomain: addresses,
})
return
}
w.Write([]byte("{}"))
}
}
type derpConfig struct {
PrivateKey key.NodePrivate
}
func readDERPConfig(path string) (*derpConfig, error) {
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return writeNewDERPConfig(path)
}
return nil, err
}
var config derpConfig
err = json.Unmarshal(content, &config)
if err != nil {
return nil, err
}
return &config, nil
}
func writeNewDERPConfig(path string) (*derpConfig, error) {
newKey := key.NewNode()
err := os.MkdirAll(filepath.Dir(path), 0o777)
if err != nil {
return nil, err
}
config := derpConfig{
PrivateKey: newKey,
}
content, err := json.Marshal(config)
if err != nil {
return nil, err
}
err = os.WriteFile(path, content, 0o644)
if err != nil {
return nil, err
}
return &config, nil
}
func (d *Service) loopSTUNPacket(packetConn *net.UDPConn) {
buffer := make([]byte, 65535)
oob := make([]byte, 1024)
var (
n int
oobN int
addrPort netip.AddrPort
err error
)
for {
n, oobN, _, addrPort, err = packetConn.ReadMsgUDPAddrPort(buffer, oob)
if err != nil {
if E.IsClosedOrCanceled(err) {
return
}
time.Sleep(time.Second)
continue
}
if !stun.Is(buffer[:n]) {
continue
}
txid, err := stun.ParseBindingRequest(buffer[:n])
if err != nil {
continue
}
packetConn.WriteMsgUDPAddrPort(stun.Response(txid, addrPort), oob[:oobN], addrPort)
}
}

View File

@ -0,0 +1,648 @@
//go:build linux
package resolved
import (
"context"
"errors"
"fmt"
"net/netip"
"os"
"os/user"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"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"
M "github.com/sagernet/sing/common/metadata"
"github.com/godbus/dbus/v5"
mDNS "github.com/miekg/dns"
)
type resolve1Manager Service
type Address struct {
IfIndex int32
Family int32
Address []byte
}
type Name struct {
IfIndex int32
Hostname string
}
type ResourceRecord struct {
IfIndex int32
Type uint16
Class uint16
Data []byte
}
type SRVRecord struct {
Priority uint16
Weight uint16
Port uint16
Hostname string
Addresses []Address
CNAME string
}
type TXTRecord []byte
type LinkDNS struct {
Family int32
Address []byte
}
type LinkDNSEx struct {
Family int32
Address []byte
Port uint16
Name string
}
type LinkDomain struct {
Domain string
RoutingOnly bool
}
func (t *resolve1Manager) getLink(ifIndex int32) (*TransportLink, *dbus.Error) {
link, loaded := t.links[ifIndex]
if !loaded {
link = &TransportLink{}
t.links[ifIndex] = link
iif, err := t.network.InterfaceFinder().ByIndex(int(ifIndex))
if err != nil {
return nil, wrapError(err)
}
link.iif = iif
}
return link, nil
}
func (t *resolve1Manager) getSenderProcess(sender dbus.Sender) (int32, error) {
var senderPid int32
dbusObject := t.systemBus.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
if dbusObject == nil {
return 0, E.New("missing dbus object")
}
err := dbusObject.Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, string(sender)).Store(&senderPid)
if err != nil {
return 0, E.Cause(err, "GetConnectionUnixProcessID")
}
return senderPid, nil
}
func (t *resolve1Manager) createMetadata(sender dbus.Sender) adapter.InboundContext {
var metadata adapter.InboundContext
metadata.Inbound = t.Tag()
metadata.InboundType = C.TypeResolved
senderPid, err := t.getSenderProcess(sender)
if err != nil {
return metadata
}
var processInfo process.Info
metadata.ProcessInfo = &processInfo
processInfo.ProcessID = uint32(senderPid)
processPath, err := os.Readlink(F.ToString("/proc/", senderPid, "/exe"))
if err == nil {
processInfo.ProcessPath = processPath
} else {
processPath, err = os.Readlink(F.ToString("/proc/", senderPid, "/comm"))
if err == nil {
processInfo.ProcessPath = processPath
}
}
var uidFound bool
statusContent, err := os.ReadFile(F.ToString("/proc/", senderPid, "/status"))
if err == nil {
for _, line := range strings.Split(string(statusContent), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "Uid:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
uid, parseErr := strconv.ParseUint(fields[1], 10, 32)
if parseErr != nil {
break
}
processInfo.UserId = int32(uid)
uidFound = true
if osUser, _ := user.LookupId(F.ToString(uid)); osUser != nil {
processInfo.User = osUser.Username
}
break
}
}
}
}
if !uidFound {
metadata.ProcessInfo.UserId = -1
}
return metadata
}
func (t *resolve1Manager) log(sender dbus.Sender, message ...any) {
metadata := t.createMetadata(sender)
if metadata.ProcessInfo != nil {
var prefix string
if metadata.ProcessInfo.ProcessPath != "" {
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
} else if metadata.ProcessInfo.User != "" {
prefix = F.ToString("user:", metadata.ProcessInfo.User)
} else if metadata.ProcessInfo.UserId != 0 {
prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
}
t.logger.Info("(", prefix, ") ", F.ToString(message...))
} else {
t.logger.Info(F.ToString(message...))
}
}
func (t *resolve1Manager) logRequest(sender dbus.Sender, message ...any) context.Context {
ctx := log.ContextWithNewID(t.ctx)
metadata := t.createMetadata(sender)
if metadata.ProcessInfo != nil {
var prefix string
if metadata.ProcessInfo.ProcessPath != "" {
prefix = filepath.Base(metadata.ProcessInfo.ProcessPath)
} else if metadata.ProcessInfo.User != "" {
prefix = F.ToString("user:", metadata.ProcessInfo.User)
} else if metadata.ProcessInfo.UserId != 0 {
prefix = F.ToString("uid:", metadata.ProcessInfo.UserId)
}
t.logger.InfoContext(ctx, "(", prefix, ") ", F.ToString(message...))
} else {
t.logger.InfoContext(ctx, F.ToString(message...))
}
return adapter.WithContext(ctx, &metadata)
}
func familyToString(family int32) string {
switch family {
case syscall.AF_UNSPEC:
return "AF_UNSPEC"
case syscall.AF_INET:
return "AF_INET"
case syscall.AF_INET6:
return "AF_INET6"
default:
return F.ToString(family)
}
}
func (t *resolve1Manager) ResolveHostname(sender dbus.Sender, ifIndex int32, hostname string, family int32, flags uint64) (addresses []Address, canonical string, outflags uint64, err *dbus.Error) {
t.linkAccess.Lock()
link, err := t.getLink(ifIndex)
if err != nil {
return
}
t.linkAccess.Unlock()
var strategy C.DomainStrategy
switch family {
case syscall.AF_UNSPEC:
strategy = C.DomainStrategyAsIS
case syscall.AF_INET:
strategy = C.DomainStrategyIPv4Only
case syscall.AF_INET6:
strategy = C.DomainStrategyIPv6Only
}
ctx := t.logRequest(sender, "ResolveHostname ", link.iif.Name, " ", hostname, " ", familyToString(family), " ", flags)
responseAddresses, lookupErr := t.dnsRouter.Lookup(ctx, hostname, adapter.DNSQueryOptions{
LookupStrategy: strategy,
})
if lookupErr != nil {
err = wrapError(err)
return
}
addresses = common.Map(responseAddresses, func(it netip.Addr) Address {
var addrFamily int32
if it.Is4() {
addrFamily = syscall.AF_INET
} else {
addrFamily = syscall.AF_INET6
}
return Address{
IfIndex: ifIndex,
Family: addrFamily,
Address: it.AsSlice(),
}
})
canonical = mDNS.CanonicalName(hostname)
return
}
func (t *resolve1Manager) ResolveAddress(sender dbus.Sender, ifIndex int32, family int32, address []byte, flags uint64) (names []Name, outflags uint64, err *dbus.Error) {
t.linkAccess.Lock()
link, err := t.getLink(ifIndex)
if err != nil {
return
}
t.linkAccess.Unlock()
addr, ok := netip.AddrFromSlice(address)
if !ok {
err = wrapError(E.New("invalid address"))
return
}
var nibbles []string
for i := len(address) - 1; i >= 0; i-- {
b := address[i]
nibbles = append(nibbles, fmt.Sprintf("%x", b&0x0F))
nibbles = append(nibbles, fmt.Sprintf("%x", b>>4))
}
var ptrDomain string
if addr.Is4() {
ptrDomain = strings.Join(nibbles, ".") + ".in-addr.arpa."
} else {
ptrDomain = strings.Join(nibbles, ".") + ".ip6.arpa."
}
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(ptrDomain),
Qtype: mDNS.TypePTR,
Qclass: mDNS.ClassINET,
},
},
}
ctx := t.logRequest(sender, "ResolveAddress ", link.iif.Name, familyToString(family), addr, flags)
response, lookupErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
if lookupErr != nil {
err = wrapError(err)
return
}
if response.Rcode != mDNS.RcodeSuccess {
err = rcodeError(response.Rcode)
return
}
for _, rawRR := range response.Answer {
switch rr := rawRR.(type) {
case *mDNS.PTR:
names = append(names, Name{
IfIndex: ifIndex,
Hostname: rr.Ptr,
})
}
}
return
}
func (t *resolve1Manager) ResolveRecord(sender dbus.Sender, ifIndex int32, family int32, hostname string, qClass uint16, qType uint16, flags uint64) (records []ResourceRecord, outflags uint64, err *dbus.Error) {
t.linkAccess.Lock()
link, err := t.getLink(ifIndex)
if err != nil {
return
}
t.linkAccess.Unlock()
request := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: mDNS.Fqdn(hostname),
Qtype: qType,
Qclass: qClass,
},
},
}
ctx := t.logRequest(sender, "ResolveRecord ", link.iif.Name, familyToString(family), hostname, mDNS.Class(qClass), mDNS.Type(qType), flags)
response, exchangeErr := t.dnsRouter.Exchange(ctx, request, adapter.DNSQueryOptions{})
if exchangeErr != nil {
err = wrapError(exchangeErr)
return
}
if response.Rcode != mDNS.RcodeSuccess {
err = rcodeError(response.Rcode)
return
}
for _, rr := range response.Answer {
var record ResourceRecord
record.IfIndex = ifIndex
record.Type = rr.Header().Rrtype
record.Class = rr.Header().Class
data := make([]byte, mDNS.Len(rr))
_, unpackErr := mDNS.PackRR(rr, data, 0, nil, false)
if unpackErr != nil {
err = wrapError(unpackErr)
}
record.Data = data
}
return
}
func (t *resolve1Manager) ResolveService(sender dbus.Sender, ifIndex int32, hostname string, sType string, domain string, family int32, flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err *dbus.Error) {
t.linkAccess.Lock()
link, err := t.getLink(ifIndex)
if err != nil {
return
}
t.linkAccess.Unlock()
serviceName := hostname
if hostname != "" && !strings.HasSuffix(hostname, ".") {
serviceName += "."
}
serviceName += sType
if !strings.HasSuffix(serviceName, ".") {
serviceName += "."
}
serviceName += domain
if !strings.HasSuffix(serviceName, ".") {
serviceName += "."
}
ctx := t.logRequest(sender, "ResolveService ", link.iif.Name, " ", hostname, " ", sType, " ", domain, " ", familyToString(family), " ", flags)
srvRequest := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: serviceName,
Qtype: mDNS.TypeSRV,
Qclass: mDNS.ClassINET,
},
},
}
srvResponse, exchangeErr := t.dnsRouter.Exchange(ctx, srvRequest, adapter.DNSQueryOptions{})
if exchangeErr != nil {
err = wrapError(exchangeErr)
return
}
if srvResponse.Rcode != mDNS.RcodeSuccess {
err = rcodeError(srvResponse.Rcode)
return
}
txtRequest := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Question: []mDNS.Question{
{
Name: serviceName,
Qtype: mDNS.TypeTXT,
Qclass: mDNS.ClassINET,
},
},
}
txtResponse, exchangeErr := t.dnsRouter.Exchange(ctx, txtRequest, adapter.DNSQueryOptions{})
if exchangeErr != nil {
err = wrapError(exchangeErr)
return
}
for _, rawRR := range srvResponse.Answer {
switch rr := rawRR.(type) {
case *mDNS.SRV:
var srvRecord SRVRecord
srvRecord.Priority = rr.Priority
srvRecord.Weight = rr.Weight
srvRecord.Port = rr.Port
srvRecord.Hostname = rr.Target
var strategy C.DomainStrategy
switch family {
case syscall.AF_UNSPEC:
strategy = C.DomainStrategyAsIS
case syscall.AF_INET:
strategy = C.DomainStrategyIPv4Only
case syscall.AF_INET6:
strategy = C.DomainStrategyIPv6Only
}
addrs, lookupErr := t.dnsRouter.Lookup(ctx, rr.Target, adapter.DNSQueryOptions{
LookupStrategy: strategy,
})
if lookupErr == nil {
srvRecord.Addresses = common.Map(addrs, func(it netip.Addr) Address {
var addrFamily int32
if it.Is4() {
addrFamily = syscall.AF_INET
} else {
addrFamily = syscall.AF_INET6
}
return Address{
IfIndex: ifIndex,
Family: addrFamily,
Address: it.AsSlice(),
}
})
}
for _, a := range srvResponse.Answer {
if cname, ok := a.(*mDNS.CNAME); ok && cname.Header().Name == rr.Target {
srvRecord.CNAME = cname.Target
break
}
}
srvData = append(srvData, srvRecord)
}
}
for _, rawRR := range txtResponse.Answer {
switch rr := rawRR.(type) {
case *mDNS.TXT:
data := make([]byte, mDNS.Len(rr))
_, packErr := mDNS.PackRR(rr, data, 0, nil, false)
if packErr == nil {
txtData = append(txtData, data)
}
}
}
canonicalName = mDNS.CanonicalName(hostname)
canonicalType = mDNS.CanonicalName(sType)
canonicalDomain = mDNS.CanonicalName(domain)
return
}
func (t *resolve1Manager) SetLinkDNS(sender dbus.Sender, ifIndex int32, addresses []LinkDNS) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return wrapError(err)
}
link.address = addresses
if len(addresses) > 0 {
t.log(sender, "SetLinkDNS ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNS) string {
return M.AddrFromIP(it.Address).String()
}), ", "))
} else {
t.log(sender, "SetLinkDNS ", link.iif.Name, " (empty)")
}
return t.postUpdate(link)
}
func (t *resolve1Manager) SetLinkDNSEx(sender dbus.Sender, ifIndex int32, addresses []LinkDNSEx) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return wrapError(err)
}
link.addressEx = addresses
if len(addresses) > 0 {
t.log(sender, "SetLinkDNSEx ", link.iif.Name, " ", strings.Join(common.Map(addresses, func(it LinkDNSEx) string {
return M.SocksaddrFrom(M.AddrFromIP(it.Address), it.Port).String()
}), ", "))
} else {
t.log(sender, "SetLinkDNSEx ", link.iif.Name, " (empty)")
}
return t.postUpdate(link)
}
func (t *resolve1Manager) SetLinkDomains(sender dbus.Sender, ifIndex int32, domains []LinkDomain) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return wrapError(err)
}
link.domain = domains
if len(domains) > 0 {
t.log(sender, "SetLinkDomains ", link.iif.Name, " ", strings.Join(common.Map(domains, func(domain LinkDomain) string {
if !domain.RoutingOnly {
return domain.Domain
} else {
return "~" + domain.Domain
}
}), ", "))
} else {
t.log(sender, "SetLinkDomains ", link.iif.Name, " (empty)")
}
return t.postUpdate(link)
}
func (t *resolve1Manager) SetLinkDefaultRoute(sender dbus.Sender, ifIndex int32, defaultRoute bool) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return err
}
link.defaultRoute = defaultRoute
if defaultRoute {
t.defaultRouteSequence = append(common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex }), ifIndex)
} else {
t.defaultRouteSequence = common.Filter(t.defaultRouteSequence, func(it int32) bool { return it != ifIndex })
}
var defaultRouteString string
if defaultRoute {
defaultRouteString = "yes"
} else {
defaultRouteString = "no"
}
t.log(sender, "SetLinkDefaultRoute ", link.iif.Name, " ", defaultRouteString)
return t.postUpdate(link)
}
func (t *resolve1Manager) SetLinkLLMNR(ifIndex int32, llmnrMode string) *dbus.Error {
return nil
}
func (t *resolve1Manager) SetLinkMulticastDNS(ifIndex int32, mdnsMode string) *dbus.Error {
return nil
}
func (t *resolve1Manager) SetLinkDNSOverTLS(sender dbus.Sender, ifIndex int32, dotMode string) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return wrapError(err)
}
switch dotMode {
case "yes":
link.dnsOverTLS = true
case "":
dotMode = "no"
fallthrough
case "opportunistic", "no":
link.dnsOverTLS = false
}
t.log(sender, "SetLinkDNSOverTLS ", link.iif.Name, " ", dotMode)
return t.postUpdate(link)
}
func (t *resolve1Manager) SetLinkDNSSEC(ifIndex int32, dnssecMode string) *dbus.Error {
return nil
}
func (t *resolve1Manager) SetLinkDNSSECNegativeTrustAnchors(ifIndex int32, domains []string) *dbus.Error {
return nil
}
func (t *resolve1Manager) RevertLink(sender dbus.Sender, ifIndex int32) *dbus.Error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
link, err := t.getLink(ifIndex)
if err != nil {
return wrapError(err)
}
delete(t.links, ifIndex)
t.log(sender, "RevertLink ", link.iif.Name)
return t.postUpdate(link)
}
// TODO: implement RegisterService, UnregisterService
func (t *resolve1Manager) RegisterService(sender dbus.Sender, identifier string, nameTemplate string, serviceType string, port uint16, priority uint16, weight uint16, txtRecords []TXTRecord) (objectPath dbus.ObjectPath, dbusErr *dbus.Error) {
return "", wrapError(E.New("not implemented"))
}
func (t *resolve1Manager) UnregisterService(sender dbus.Sender, servicePath dbus.ObjectPath) error {
return wrapError(E.New("not implemented"))
}
func (t *resolve1Manager) ResetStatistics() *dbus.Error {
return nil
}
func (t *resolve1Manager) FlushCaches(sender dbus.Sender) *dbus.Error {
t.dnsRouter.ClearCache()
t.log(sender, "FlushCaches")
return nil
}
func (t *resolve1Manager) ResetServerFeatures() *dbus.Error {
return nil
}
func (t *resolve1Manager) postUpdate(link *TransportLink) *dbus.Error {
if t.updateCallback != nil {
return wrapError(t.updateCallback(link))
}
return nil
}
func rcodeError(rcode int) *dbus.Error {
return dbus.NewError("org.freedesktop.resolve1.DnsError."+mDNS.RcodeToString[rcode], []any{mDNS.RcodeToString[rcode]})
}
func wrapError(err error) *dbus.Error {
if err == nil {
return nil
}
var rcode dns.RcodeError
if errors.As(err, &rcode) {
return rcodeError(int(rcode))
}
return dbus.MakeFailedError(err)
}

252
service/resolved/service.go Normal file
View File

@ -0,0 +1,252 @@
//go:build linux
package resolved
import (
"context"
"net"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/listener"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"github.com/godbus/dbus/v5"
mDNS "github.com/miekg/dns"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
network adapter.NetworkManager
dnsRouter adapter.DNSRouter
listener *listener.Listener
systemBus *dbus.Conn
linkAccess sync.RWMutex
links map[int32]*TransportLink
defaultRouteSequence []int32
networkUpdateCallback *list.Element[tun.NetworkUpdateCallback]
updateCallback func(*TransportLink) error
deleteCallback func(*TransportLink)
}
type TransportLink struct {
iif *control.Interface
address []LinkDNS
addressEx []LinkDNSEx
domain []LinkDomain
defaultRoute bool
dnsOverTLS bool
// dnsOverTLSFallback bool
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
inbound := &Service{
Adapter: boxService.NewAdapter(C.TypeResolved, tag),
ctx: ctx,
logger: logger,
network: service.FromContext[adapter.NetworkManager](ctx),
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
links: make(map[int32]*TransportLink),
}
inbound.listener = listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP, N.NetworkUDP},
Listen: options.ListenOptions,
ConnectionHandler: inbound,
OOBPacketHandler: inbound,
ThreadUnsafePacketWriter: true,
})
return inbound, nil
}
func (i *Service) Start(stage adapter.StartStage) error {
switch stage {
case adapter.StartStateInitialize:
inboundManager := service.FromContext[adapter.ServiceManager](i.ctx)
for _, transport := range inboundManager.Services() {
if transport.Type() == C.TypeResolved && transport != i {
return E.New("multiple resolved service are not supported")
}
}
case adapter.StartStateStart:
err := i.listener.Start()
if err != nil {
return err
}
systemBus, err := dbus.SystemBus()
if err != nil {
return err
}
i.systemBus = systemBus
err = systemBus.Export((*resolve1Manager)(i), "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager")
if err != nil {
return err
}
reply, err := systemBus.RequestName("org.freedesktop.resolve1", dbus.NameFlagDoNotQueue)
if err != nil {
return err
}
switch reply {
case dbus.RequestNameReplyPrimaryOwner:
case dbus.RequestNameReplyExists:
return E.New("D-Bus object already exists, maybe real resolved is running")
default:
return E.New("unknown request name reply: ", reply)
}
i.networkUpdateCallback = i.network.NetworkMonitor().RegisterCallback(i.onNetworkUpdate)
}
return nil
}
func (i *Service) Close() error {
if i.networkUpdateCallback != nil {
i.network.NetworkMonitor().UnregisterCallback(i.networkUpdateCallback)
}
if i.systemBus != nil {
i.systemBus.ReleaseName("org.freedesktop.resolve1")
i.systemBus.Close()
}
return i.listener.Close()
}
func (i *Service) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
metadata.Inbound = i.Tag()
metadata.InboundType = i.Type()
metadata.Destination = M.Socksaddr{}
for {
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
err := dnsOutbound.HandleStreamDNSRequest(ctx, i.dnsRouter, conn, metadata)
if err != nil {
N.CloseOnHandshakeFailure(conn, onClose, err)
return
}
}
}
func (i *Service) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
go i.exchangePacket(buffer, oob, source)
}
func (i *Service) exchangePacket(buffer *buf.Buffer, oob []byte, source M.Socksaddr) {
ctx := log.ContextWithNewID(i.ctx)
err := i.exchangePacket0(ctx, buffer, oob, source)
if err != nil {
i.logger.ErrorContext(ctx, "process DNS packet: ", err)
}
}
func (i *Service) exchangePacket0(ctx context.Context, buffer *buf.Buffer, oob []byte, source M.Socksaddr) error {
var message mDNS.Msg
err := message.Unpack(buffer.Bytes())
buffer.Release()
if err != nil {
return E.Cause(err, "unpack request")
}
var metadata adapter.InboundContext
metadata.Source = source
response, err := i.dnsRouter.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{})
if err != nil {
return err
}
responseBuffer, err := dns.TruncateDNSMessage(&message, response, 0)
if err != nil {
return err
}
defer responseBuffer.Release()
_, _, err = i.listener.UDPConn().WriteMsgUDPAddrPort(responseBuffer.Bytes(), oob, source.AddrPort())
return err
}
func (i *Service) onNetworkUpdate() {
i.linkAccess.Lock()
defer i.linkAccess.Unlock()
var deleteIfIndex []int
for ifIndex, link := range i.links {
iif, err := i.network.InterfaceFinder().ByIndex(int(ifIndex))
if err != nil || iif != link.iif {
deleteIfIndex = append(deleteIfIndex, int(ifIndex))
}
i.defaultRouteSequence = common.Filter(i.defaultRouteSequence, func(it int32) bool {
return it != ifIndex
})
if i.deleteCallback != nil {
i.deleteCallback(link)
}
}
for _, ifIndex := range deleteIfIndex {
delete(i.links, int32(ifIndex))
}
}
func (conf *TransportLink) nameList(ndots int, name string) []string {
search := common.Map(common.Filter(conf.domain, func(it LinkDomain) bool {
return !it.RoutingOnly
}), func(it LinkDomain) string {
return it.Domain
})
l := len(name)
rooted := l > 0 && name[l-1] == '.'
if l > 254 || l == 254 && !rooted {
return nil
}
if rooted {
if avoidDNS(name) {
return nil
}
return []string{name}
}
hasNdots := strings.Count(name, ".") >= ndots
name += "."
// l++
names := make([]string, 0, 1+len(search))
if hasNdots && !avoidDNS(name) {
names = append(names, name)
}
for _, suffix := range search {
fqdn := name + suffix
if !avoidDNS(fqdn) && len(fqdn) <= 254 {
names = append(names, fqdn)
}
}
if !hasNdots && !avoidDNS(name) {
names = append(names, name)
}
return names
}
func avoidDNS(name string) bool {
if name == "" {
return true
}
if name[len(name)-1] == '.' {
name = name[:len(name)-1]
}
return strings.HasSuffix(name, ".onion")
}

27
service/resolved/stub.go Normal file
View File

@ -0,0 +1,27 @@
//go:build !linux
package resolved
import (
"context"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.ResolvedServiceOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedServiceOptions) (adapter.Service, error) {
return nil, E.New("resolved service is only supported on Linux")
})
}
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
return nil, E.New("resolved DNS server is only supported on Linux")
})
}

View File

@ -0,0 +1,297 @@
//go:build linux
package resolved
import (
"context"
"net/netip"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/dns/transport"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/service"
mDNS "github.com/miekg/dns"
)
func RegisterTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.ResolvedDNSServerOptions](registry, C.TypeResolved, NewTransport)
}
var _ adapter.DNSTransport = (*Transport)(nil)
type Transport struct {
dns.TransportAdapter
ctx context.Context
logger logger.ContextLogger
serviceTag string
acceptDefaultResolvers bool
ndots int
timeout time.Duration
attempts int
rotate bool
service *Service
linkAccess sync.RWMutex
linkServers map[*TransportLink]*LinkServers
}
type LinkServers struct {
Link *TransportLink
Servers []adapter.DNSTransport
serverOffset uint32
}
func (c *LinkServers) ServerOffset(rotate bool) uint32 {
if rotate {
return atomic.AddUint32(&c.serverOffset, 1) - 1
}
return 0
}
func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.ResolvedDNSServerOptions) (adapter.DNSTransport, error) {
return &Transport{
TransportAdapter: dns.NewTransportAdapter(C.DNSTypeDHCP, tag, nil),
ctx: ctx,
logger: logger,
serviceTag: options.Service,
acceptDefaultResolvers: options.AcceptDefaultResolvers,
// ndots: options.NDots,
// timeout: time.Duration(options.Timeout),
// attempts: options.Attempts,
// rotate: options.Rotate,
ndots: 1,
timeout: 5 * time.Second,
attempts: 2,
linkServers: make(map[*TransportLink]*LinkServers),
}, nil
}
func (t *Transport) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateInitialize {
return nil
}
serviceManager := service.FromContext[adapter.ServiceManager](t.ctx)
service, loaded := serviceManager.Get(t.serviceTag)
if !loaded {
return E.New("service not found: ", t.serviceTag)
}
resolvedInbound, isResolved := service.(*Service)
if !isResolved {
return E.New("service is not resolved: ", t.serviceTag)
}
resolvedInbound.updateCallback = t.updateTransports
resolvedInbound.deleteCallback = t.deleteTransport
t.service = resolvedInbound
return nil
}
func (t *Transport) Close() error {
t.linkAccess.RLock()
defer t.linkAccess.RUnlock()
for _, servers := range t.linkServers {
for _, server := range servers.Servers {
server.Close()
}
}
return nil
}
func (t *Transport) updateTransports(link *TransportLink) error {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
if servers, loaded := t.linkServers[link]; loaded {
for _, server := range servers.Servers {
server.Close()
}
}
serverDialer := common.Must1(dialer.NewDefault(t.ctx, option.DialerOptions{
BindInterface: link.iif.Name,
UDPFragmentDefault: true,
}))
var transports []adapter.DNSTransport
for _, address := range link.address {
serverAddr, ok := netip.AddrFromSlice(address.Address)
if !ok {
return os.ErrInvalid
}
if link.dnsOverTLS {
tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
Enabled: true,
ServerName: serverAddr.String(),
}))
transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53), tlsConfig))
} else {
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, 53)))
}
}
for _, address := range link.addressEx {
serverAddr, ok := netip.AddrFromSlice(address.Address)
if !ok {
return os.ErrInvalid
}
if link.dnsOverTLS {
var serverName string
if address.Name != "" {
serverName = address.Name
} else {
serverName = serverAddr.String()
}
tlsConfig := common.Must1(tls.NewClient(t.ctx, serverAddr.String(), option.OutboundTLSOptions{
Enabled: true,
ServerName: serverName,
}))
transports = append(transports, transport.NewTLSRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port), tlsConfig))
} else {
transports = append(transports, transport.NewUDPRaw(t.logger, t.TransportAdapter, serverDialer, M.SocksaddrFrom(serverAddr, address.Port)))
}
}
t.linkServers[link] = &LinkServers{
Link: link,
Servers: transports,
}
return nil
}
func (t *Transport) deleteTransport(link *TransportLink) {
t.linkAccess.Lock()
defer t.linkAccess.Unlock()
servers, loaded := t.linkServers[link]
if !loaded {
return
}
for _, server := range servers.Servers {
server.Close()
}
delete(t.linkServers, link)
}
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
var selectedLink *TransportLink
t.service.linkAccess.RLock()
for _, link := range t.service.links {
for _, domain := range link.domain {
if domain.Domain == "." && domain.RoutingOnly && !t.acceptDefaultResolvers {
continue
}
if strings.HasSuffix(question.Name, domain.Domain) {
selectedLink = link
}
}
}
if selectedLink == nil && t.acceptDefaultResolvers {
for l := len(t.service.defaultRouteSequence); l > 0; l-- {
selectedLink = t.service.links[t.service.defaultRouteSequence[l-1]]
if len(selectedLink.address) > 0 || len(selectedLink.addressEx) > 0 {
break
}
}
}
t.service.linkAccess.RUnlock()
if selectedLink == nil {
return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
}
t.linkAccess.RLock()
servers := t.linkServers[selectedLink]
t.linkAccess.RUnlock()
if len(servers.Servers) == 0 {
return dns.FixedResponseStatus(message, mDNS.RcodeNameError), nil
}
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
return t.exchangeParallel(ctx, servers, message)
} else {
return t.exchangeSingleRequest(ctx, servers, message)
}
}
func (t *Transport) exchangeSingleRequest(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
var lastErr error
for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
response, err := t.tryOneName(ctx, servers, message, fqdn)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
func (t *Transport) tryOneName(ctx context.Context, servers *LinkServers, message *mDNS.Msg, fqdn string) (*mDNS.Msg, error) {
serverOffset := servers.ServerOffset(t.rotate)
sLen := uint32(len(servers.Servers))
var lastErr error
for i := 0; i < t.attempts; i++ {
for j := uint32(0); j < sLen; j++ {
server := servers.Servers[(serverOffset+j)%sLen]
question := message.Question[0]
question.Name = fqdn
exchangeMessage := *message
exchangeMessage.Question = []mDNS.Question{question}
exchangeCtx, cancel := context.WithTimeout(ctx, t.timeout)
response, err := server.Exchange(exchangeCtx, &exchangeMessage)
cancel()
if err != nil {
lastErr = err
continue
}
return response, nil
}
}
return nil, E.Cause(lastErr, fqdn)
}
func (t *Transport) exchangeParallel(ctx context.Context, servers *LinkServers, message *mDNS.Msg) (*mDNS.Msg, error) {
returned := make(chan struct{})
defer close(returned)
type queryResult struct {
response *mDNS.Msg
err error
}
results := make(chan queryResult)
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, servers, message, fqdn)
select {
case results <- queryResult{response, err}:
case <-returned:
}
}
queryCtx, queryCancel := context.WithCancel(ctx)
defer queryCancel()
var nameCount int
for _, fqdn := range servers.Link.nameList(t.ndots, message.Question[0].Name) {
nameCount++
go startRacer(queryCtx, fqdn)
}
var errors []error
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case result := <-results:
if result.err == nil {
return result.response, nil
}
errors = append(errors, result.err)
if len(errors) == nameCount {
return nil, E.Errors(errors...)
}
}
}
}

181
service/ssmapi/api.go Normal file
View File

@ -0,0 +1,181 @@
package ssmapi
import (
"net/http"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common/logger"
sHTTP "github.com/sagernet/sing/protocol/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
type APIServer struct {
logger logger.Logger
traffic *TrafficManager
user *UserManager
}
func NewAPIServer(logger logger.Logger, traffic *TrafficManager, user *UserManager) *APIServer {
return &APIServer{
logger: logger,
traffic: traffic,
user: user,
}
}
func (s *APIServer) Route(r chi.Router) {
r.Route("/server/v1", func(r chi.Router) {
r.Use(func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
s.logger.Debug(request.Method, " ", request.RequestURI, " ", sHTTP.SourceAddress(request))
handler.ServeHTTP(writer, request)
})
})
r.Get("/", s.getServerInfo)
r.Get("/users", s.listUser)
r.Post("/users", s.addUser)
r.Get("/users/{username}", s.getUser)
r.Put("/users/{username}", s.updateUser)
r.Delete("/users/{username}", s.deleteUser)
r.Get("/stats", s.getStats)
})
}
func (s *APIServer) getServerInfo(writer http.ResponseWriter, request *http.Request) {
render.JSON(writer, request, render.M{
"server": "sing-box " + C.Version,
"apiVersion": "v1",
})
}
type UserObject struct {
UserName string `json:"username"`
Password string `json:"uPSK,omitempty"`
DownlinkBytes int64 `json:"downlinkBytes"`
UplinkBytes int64 `json:"uplinkBytes"`
DownlinkPackets int64 `json:"downlinkPackets"`
UplinkPackets int64 `json:"uplinkPackets"`
TCPSessions int64 `json:"tcpSessions"`
UDPSessions int64 `json:"udpSessions"`
}
func (s *APIServer) listUser(writer http.ResponseWriter, request *http.Request) {
render.JSON(writer, request, render.M{
"users": s.user.List(),
})
}
func (s *APIServer) addUser(writer http.ResponseWriter, request *http.Request) {
var addRequest struct {
UserName string `json:"username"`
Password string `json:"uPSK"`
}
err := render.DecodeJSON(request.Body, &addRequest)
if err != nil {
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, err.Error())
return
}
err = s.user.Add(addRequest.UserName, addRequest.Password)
if err != nil {
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, err.Error())
return
}
writer.WriteHeader(http.StatusCreated)
}
func (s *APIServer) getUser(writer http.ResponseWriter, request *http.Request) {
userName := chi.URLParam(request, "username")
if userName == "" {
writer.WriteHeader(http.StatusBadRequest)
return
}
uPSK, loaded := s.user.Get(userName)
if !loaded {
writer.WriteHeader(http.StatusNotFound)
return
}
user := UserObject{
UserName: userName,
Password: uPSK,
}
s.traffic.ReadUser(&user)
render.JSON(writer, request, user)
}
func (s *APIServer) updateUser(writer http.ResponseWriter, request *http.Request) {
userName := chi.URLParam(request, "username")
if userName == "" {
writer.WriteHeader(http.StatusBadRequest)
return
}
var updateRequest struct {
Password string `json:"uPSK"`
}
err := render.DecodeJSON(request.Body, &updateRequest)
if err != nil {
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, err.Error())
return
}
_, loaded := s.user.Get(userName)
if !loaded {
writer.WriteHeader(http.StatusNotFound)
return
}
err = s.user.Update(userName, updateRequest.Password)
if err != nil {
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, err.Error())
return
}
writer.WriteHeader(http.StatusNoContent)
}
func (s *APIServer) deleteUser(writer http.ResponseWriter, request *http.Request) {
userName := chi.URLParam(request, "username")
if userName == "" {
writer.WriteHeader(http.StatusBadRequest)
return
}
_, loaded := s.user.Get(userName)
if !loaded {
writer.WriteHeader(http.StatusNotFound)
return
}
err := s.user.Delete(userName)
if err != nil {
render.Status(request, http.StatusBadRequest)
render.PlainText(writer, request, err.Error())
return
}
writer.WriteHeader(http.StatusNoContent)
}
func (s *APIServer) getStats(writer http.ResponseWriter, request *http.Request) {
requireClear := chi.URLParam(request, "clear") == "true"
users := s.user.List()
s.traffic.ReadUsers(users)
for i := range users {
users[i].Password = ""
}
uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.traffic.ReadGlobal()
if requireClear {
s.traffic.Clear()
}
render.JSON(writer, request, render.M{
"uplinkBytes": uplinkBytes,
"downlinkBytes": downlinkBytes,
"uplinkPackets": uplinkPackets,
"downlinkPackets": downlinkPackets,
"tcpSessions": tcpSessions,
"udpSessions": udpSessions,
"users": users,
})
}

117
service/ssmapi/server.go Normal file
View File

@ -0,0 +1,117 @@
package ssmapi
import (
"context"
"errors"
"net/http"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
"github.com/go-chi/chi/v5"
"golang.org/x/net/http2"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.SSMAPIServiceOptions](registry, C.TypeSSMAPI, NewService)
}
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
chiRouter := chi.NewRouter()
s := &Service{
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
ctx: ctx,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
httpServer: &http.Server{
Handler: chiRouter,
},
}
inboundManager := service.FromContext[adapter.InboundManager](ctx)
if options.Servers.Size() == 0 {
return nil, E.New("missing servers")
}
for i, entry := range options.Servers.Entries() {
inbound, loaded := inboundManager.Get(entry.Value)
if !loaded {
return nil, E.New("parse SSM server[", i, "]: inbound ", entry.Value, " not found")
}
managedServer, isManaged := inbound.(adapter.ManagedSSMServer)
if !isManaged {
return nil, E.New("parse SSM server[", i, "]: inbound/", inbound.Type(), "[", inbound.Tag(), "] is not a SSM server")
}
traffic := NewTrafficManager()
managedServer.SetTracker(traffic)
user := NewUserManager(managedServer, traffic)
chiRouter.Route(entry.Key, NewAPIServer(logger, traffic, user).Route)
}
if options.TLS != nil {
tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
s.tlsConfig = tlsConfig
}
return s, nil
}
func (s *Service) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
if s.tlsConfig != nil {
err := s.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
}
tcpListener, err := s.listener.ListenTCP()
if err != nil {
return err
}
if s.tlsConfig != nil {
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
}
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
}
go func() {
err = s.httpServer.Serve(tcpListener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("serve error: ", err)
}
}()
return nil
}
func (s *Service) Close() error {
return common.Close(
common.PtrOrNil(s.httpServer),
common.PtrOrNil(s.listener),
s.tlsConfig,
)
}

215
service/ssmapi/traffic.go Normal file
View File

@ -0,0 +1,215 @@
package ssmapi
import (
"net"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/bufio"
N "github.com/sagernet/sing/common/network"
)
var _ adapter.SSMTracker = (*TrafficManager)(nil)
type TrafficManager struct {
globalUplink atomic.Int64
globalDownlink atomic.Int64
globalUplinkPackets atomic.Int64
globalDownlinkPackets atomic.Int64
globalTCPSessions atomic.Int64
globalUDPSessions atomic.Int64
userAccess sync.Mutex
userUplink map[string]*atomic.Int64
userDownlink map[string]*atomic.Int64
userUplinkPackets map[string]*atomic.Int64
userDownlinkPackets map[string]*atomic.Int64
userTCPSessions map[string]*atomic.Int64
userUDPSessions map[string]*atomic.Int64
}
func NewTrafficManager() *TrafficManager {
manager := &TrafficManager{
userUplink: make(map[string]*atomic.Int64),
userDownlink: make(map[string]*atomic.Int64),
userUplinkPackets: make(map[string]*atomic.Int64),
userDownlinkPackets: make(map[string]*atomic.Int64),
userTCPSessions: make(map[string]*atomic.Int64),
userUDPSessions: make(map[string]*atomic.Int64),
}
return manager
}
func (s *TrafficManager) UpdateUsers(users []string) {
s.userAccess.Lock()
defer s.userAccess.Unlock()
newUserUplink := make(map[string]*atomic.Int64)
newUserDownlink := make(map[string]*atomic.Int64)
newUserUplinkPackets := make(map[string]*atomic.Int64)
newUserDownlinkPackets := make(map[string]*atomic.Int64)
newUserTCPSessions := make(map[string]*atomic.Int64)
newUserUDPSessions := make(map[string]*atomic.Int64)
for _, user := range users {
newUserUplink[user] = s.userUplinkPackets[user]
newUserDownlink[user] = s.userDownlinkPackets[user]
newUserUplinkPackets[user] = s.userUplinkPackets[user]
newUserDownlinkPackets[user] = s.userDownlinkPackets[user]
newUserTCPSessions[user] = s.userTCPSessions[user]
newUserUDPSessions[user] = s.userUDPSessions[user]
}
s.userUplink = newUserUplink
s.userDownlink = newUserDownlink
s.userUplinkPackets = newUserUplinkPackets
s.userDownlinkPackets = newUserDownlinkPackets
s.userTCPSessions = newUserTCPSessions
s.userUDPSessions = newUserUDPSessions
}
func (s *TrafficManager) userCounter(user string) (*atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64) {
s.userAccess.Lock()
defer s.userAccess.Unlock()
upCounter, loaded := s.userUplink[user]
if !loaded {
upCounter = new(atomic.Int64)
s.userUplink[user] = upCounter
}
downCounter, loaded := s.userDownlink[user]
if !loaded {
downCounter = new(atomic.Int64)
s.userDownlink[user] = downCounter
}
upPacketsCounter, loaded := s.userUplinkPackets[user]
if !loaded {
upPacketsCounter = new(atomic.Int64)
s.userUplinkPackets[user] = upPacketsCounter
}
downPacketsCounter, loaded := s.userDownlinkPackets[user]
if !loaded {
downPacketsCounter = new(atomic.Int64)
s.userDownlinkPackets[user] = downPacketsCounter
}
tcpSessionsCounter, loaded := s.userTCPSessions[user]
if !loaded {
tcpSessionsCounter = new(atomic.Int64)
s.userTCPSessions[user] = tcpSessionsCounter
}
udpSessionsCounter, loaded := s.userUDPSessions[user]
if !loaded {
udpSessionsCounter = new(atomic.Int64)
s.userUDPSessions[user] = udpSessionsCounter
}
return upCounter, downCounter, upPacketsCounter, downPacketsCounter, tcpSessionsCounter, udpSessionsCounter
}
func (s *TrafficManager) TrackConnection(conn net.Conn, metadata adapter.InboundContext) net.Conn {
s.globalTCPSessions.Add(1)
var readCounter []*atomic.Int64
var writeCounter []*atomic.Int64
readCounter = append(readCounter, &s.globalUplink)
writeCounter = append(writeCounter, &s.globalDownlink)
upCounter, downCounter, _, _, tcpSessionCounter, _ := s.userCounter(metadata.User)
readCounter = append(readCounter, upCounter)
writeCounter = append(writeCounter, downCounter)
tcpSessionCounter.Add(1)
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
}
func (s *TrafficManager) TrackPacketConnection(conn N.PacketConn, metadata adapter.InboundContext) N.PacketConn {
s.globalUDPSessions.Add(1)
var readCounter []*atomic.Int64
var readPacketCounter []*atomic.Int64
var writeCounter []*atomic.Int64
var writePacketCounter []*atomic.Int64
readCounter = append(readCounter, &s.globalUplink)
writeCounter = append(writeCounter, &s.globalDownlink)
readPacketCounter = append(readPacketCounter, &s.globalUplinkPackets)
writePacketCounter = append(writePacketCounter, &s.globalDownlinkPackets)
upCounter, downCounter, upPacketsCounter, downPacketsCounter, _, udpSessionCounter := s.userCounter(metadata.User)
readCounter = append(readCounter, upCounter)
writeCounter = append(writeCounter, downCounter)
readPacketCounter = append(readPacketCounter, upPacketsCounter)
writePacketCounter = append(writePacketCounter, downPacketsCounter)
udpSessionCounter.Add(1)
return bufio.NewInt64CounterPacketConn(conn, append(readCounter, readPacketCounter...), append(writeCounter, writePacketCounter...))
}
func (s *TrafficManager) ReadUser(user *UserObject) {
s.userAccess.Lock()
defer s.userAccess.Unlock()
s.readUser(user)
}
func (s *TrafficManager) readUser(user *UserObject) {
if counter, loaded := s.userUplink[user.UserName]; loaded {
user.UplinkBytes = counter.Load()
}
if counter, loaded := s.userDownlink[user.UserName]; loaded {
user.DownlinkBytes = counter.Load()
}
if counter, loaded := s.userUplinkPackets[user.UserName]; loaded {
user.UplinkPackets = counter.Load()
}
if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded {
user.DownlinkPackets = counter.Load()
}
if counter, loaded := s.userTCPSessions[user.UserName]; loaded {
user.TCPSessions = counter.Load()
}
if counter, loaded := s.userUDPSessions[user.UserName]; loaded {
user.UDPSessions = counter.Load()
}
}
func (s *TrafficManager) ReadUsers(users []*UserObject) {
s.userAccess.Lock()
defer s.userAccess.Unlock()
for _, user := range users {
s.readUser(user)
}
return
}
func (s *TrafficManager) ReadGlobal() (
uplinkBytes int64,
downlinkBytes int64,
uplinkPackets int64,
downlinkPackets int64,
tcpSessions int64,
udpSessions int64,
) {
return s.globalUplink.Load(),
s.globalDownlink.Load(),
s.globalUplinkPackets.Load(),
s.globalDownlinkPackets.Load(),
s.globalTCPSessions.Load(),
s.globalUDPSessions.Load()
}
func (s *TrafficManager) Clear() {
s.globalUplink.Store(0)
s.globalDownlink.Store(0)
s.globalUplinkPackets.Store(0)
s.globalDownlinkPackets.Store(0)
s.globalTCPSessions.Store(0)
s.globalUDPSessions.Store(0)
s.userAccess.Lock()
defer s.userAccess.Unlock()
for _, counter := range s.userUplink {
counter.Store(0)
}
for _, counter := range s.userDownlink {
counter.Store(0)
}
for _, counter := range s.userUplinkPackets {
counter.Store(0)
}
for _, counter := range s.userDownlinkPackets {
counter.Store(0)
}
for _, counter := range s.userTCPSessions {
counter.Store(0)
}
for _, counter := range s.userUDPSessions {
counter.Store(0)
}
}

85
service/ssmapi/user.go Normal file
View File

@ -0,0 +1,85 @@
package ssmapi
import (
"sync"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
)
type UserManager struct {
access sync.Mutex
usersMap map[string]string
server adapter.ManagedSSMServer
trafficManager *TrafficManager
}
func NewUserManager(inbound adapter.ManagedSSMServer, trafficManager *TrafficManager) *UserManager {
return &UserManager{
usersMap: make(map[string]string),
server: inbound,
trafficManager: trafficManager,
}
}
func (m *UserManager) postUpdate() error {
users := make([]string, 0, len(m.usersMap))
uPSKs := make([]string, 0, len(m.usersMap))
for username, password := range m.usersMap {
users = append(users, username)
uPSKs = append(uPSKs, password)
}
err := m.server.UpdateUsers(users, uPSKs)
if err != nil {
return err
}
m.trafficManager.UpdateUsers(users)
return nil
}
func (m *UserManager) List() []*UserObject {
m.access.Lock()
defer m.access.Unlock()
users := make([]*UserObject, 0, len(m.usersMap))
for username, password := range m.usersMap {
users = append(users, &UserObject{
UserName: username,
Password: password,
})
}
return users
}
func (m *UserManager) Add(username string, password string) error {
m.access.Lock()
defer m.access.Unlock()
if _, found := m.usersMap[username]; found {
return E.New("user ", username, " already exists")
}
m.usersMap[username] = password
return m.postUpdate()
}
func (m *UserManager) Get(username string) (string, bool) {
m.access.Lock()
defer m.access.Unlock()
if password, found := m.usersMap[username]; found {
return password, true
}
return "", false
}
func (m *UserManager) Update(username string, password string) error {
m.access.Lock()
defer m.access.Unlock()
m.usersMap[username] = password
return m.postUpdate()
}
func (m *UserManager) Delete(username string) error {
m.access.Lock()
defer m.access.Unlock()
delete(m.usersMap, username)
return m.postUpdate()
}

View File

@ -32,7 +32,7 @@ func TestMain(m *testing.M) {
var globalCtx context.Context var globalCtx context.Context
func init() { func init() {
globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry()) globalCtx = include.Context(context.Background())
} }
func startInstance(t *testing.T, options option.Options) *box.Box { func startInstance(t *testing.T, options option.Options) *box.Box {