Compare commits

..

86 Commits

Author SHA1 Message Date
世界
462bd95472
documentation: Bump version 2025-05-16 20:53:14 +08:00
世界
8f1abe401a
prevent creation of bind and mark controls on unsupported platforms 2025-05-16 20:53:14 +08:00
PuerNya
64fb1f21ac
documentation: Fix description of reject DNS action behavior 2025-05-16 20:29:29 +08:00
Restia-Ashbell
e432d7259f
Fix TLS record fragment 2025-05-14 09:28:54 +08:00
世界
994c8ccb23
Add missing accept_routes option for Tailscale 2025-05-14 09:28:54 +08:00
世界
6500a9584f
Add TLS record fragment support 2025-05-14 09:28:54 +08:00
世界
a68d72dfe5
release: Update Go to 1.24.3 2025-05-14 09:28:54 +08:00
世界
68322a2ede
Fix set edns0 client subnet 2025-05-14 09:28:54 +08:00
世界
deee50684b
Update minor dependencies 2025-05-14 09:28:53 +08:00
世界
a3123abf6c
Update certmagic and providers 2025-05-14 09:28:53 +08:00
世界
47130b052b
Update protobuf and grpc 2025-05-14 09:28:53 +08:00
世界
eab7fb1b7e
Add control options for listeners 2025-05-14 09:28:53 +08:00
世界
f421c0e439
Update quic-go to v0.51.0 2025-05-14 09:28:53 +08:00
世界
3670371f4b
Update utls to v1.7.2 2025-05-14 09:28:53 +08:00
世界
56717b599d
Handle EDNS version downgrade 2025-05-14 09:28:53 +08:00
世界
2de381b430
documentation: Fix anytls padding scheme description 2025-05-14 09:28:53 +08:00
安容
c0e7f2cc58
Report invalid DNS address early 2025-05-14 09:28:53 +08:00
世界
30e9a2bcaf
Fix wireguard listen_port 2025-05-14 09:28:53 +08:00
世界
1fdd0f602f
clash-api: Add more meta api 2025-05-14 09:28:52 +08:00
世界
ac2bca3bf7
Fix DNS lookup 2025-05-14 09:28:52 +08:00
世界
1ca3a80e9c
Fix fetch ECH configs 2025-05-14 09:28:52 +08:00
reletor
bfec5f040d
documentation: Minor fixes 2025-05-14 09:28:52 +08:00
caelansar
052d912e3c
Fix callback deletion in UDP transport 2025-05-14 09:28:52 +08:00
世界
ff23969e43
documentation: Try to make the play review happy 2025-05-14 09:28:52 +08:00
世界
221bba4c96
Fix missing handling of legacy domain_strategy options 2025-05-14 09:28:52 +08:00
世界
2011148ef4
Improve local DNS server 2025-05-14 09:28:52 +08:00
anytls
4d43653795
Update anytls
Co-authored-by: anytls <anytls>
2025-05-14 09:28:52 +08:00
世界
88d08e3f53
Fix DNS dialer 2025-05-14 09:28:52 +08:00
世界
4a7494da58
release: Skip override version for iOS 2025-05-14 09:28:51 +08:00
iikira
a268062c55
Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-05-14 09:28:51 +08:00
ReleTor
383b844a43
Fix fetch ECH configs 2025-05-14 09:28:51 +08:00
世界
6ed53fb1c1
Allow direct outbounds without domain_resolver 2025-05-14 09:28:51 +08:00
世界
223ea51810
Fix Tailscale dialer 2025-05-14 09:28:51 +08:00
dyhkwong
6d3b24fe33
Fix DNS over QUIC stream close 2025-05-14 09:28:50 +08:00
anytls
cc758a2d79
Update anytls
Co-authored-by: anytls <anytls>
2025-05-14 09:28:50 +08:00
Rambling2076
a11cffce08
Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-05-14 09:28:49 +08:00
世界
23f77daba2
Fail when default DNS server not found 2025-05-14 09:28:49 +08:00
世界
f899e6c246
Update gVisor to 20250319.0 2025-05-14 09:28:49 +08:00
世界
73bab537d1
Explicitly reject detour to empty direct outbounds 2025-05-14 09:28:49 +08:00
世界
2224eee136
Add netns support 2025-05-14 09:28:49 +08:00
世界
c7abc49bea
Add wildcard name support for predefined records 2025-05-14 09:28:48 +08:00
世界
77b7c8d2a5
Remove map usage in options 2025-05-14 09:28:48 +08:00
世界
9ff02d7e0a
Fix unhandled DNS loop 2025-05-14 09:28:48 +08:00
世界
16e4401060
Add wildcard-sni support for shadow-tls inbound 2025-05-14 09:28:47 +08:00
k9982874
0974764c35
Add ntp protocol sniffing 2025-05-14 09:28:47 +08:00
世界
9833c23e1e
option: Fix marshal legacy DNS options 2025-05-14 09:28:47 +08:00
世界
3bfbf3e6b2
Make domain_resolver optional when only one DNS server is configured 2025-05-14 09:28:46 +08:00
世界
f2f959bf2f
Fix DNS lookup context pollution 2025-05-14 09:28:45 +08:00
世界
b876dd92be
Fix http3 DNS server connecting to wrong address 2025-05-14 09:28:45 +08:00
Restia-Ashbell
63580f0199
documentation: Fix typo 2025-05-14 09:28:45 +08:00
anytls
2e287fa0ce
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-14 09:28:45 +08:00
k9982874
2e2ba4ec8b
Fix hosts DNS server 2025-05-14 09:28:44 +08:00
世界
d999f44d62
Fix UDP DNS server crash 2025-05-14 09:28:44 +08:00
世界
b94a46f35c
documentation: Fix missing ip_accept_any DNS rule option 2025-05-14 09:28:44 +08:00
世界
fa45f2e4fb
Fix anytls dialer usage 2025-05-14 09:28:43 +08:00
世界
073cb6fab8
Move predefined DNS server to rule action 2025-05-14 09:28:43 +08:00
世界
632e071be3
Fix domain resolver on direct outbound 2025-05-14 09:28:43 +08:00
Zephyruso
dcc7648431
Fix missing AnyTLS display name 2025-05-14 09:28:43 +08:00
anytls
ecae4869ad
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-05-14 09:28:42 +08:00
Estel
7bd83f1aa4
documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-05-14 09:28:42 +08:00
TargetLocked
771d9b49e4
Fix parsing legacy DNS options 2025-05-14 09:28:42 +08:00
世界
270cf99d9f
Fix DNS fallback 2025-05-14 09:28:42 +08:00
世界
fd20fe48d2
documentation: Fix missing hosts DNS server 2025-05-14 09:28:42 +08:00
anytls
79edcb58f8
Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-05-14 09:28:41 +08:00
ReleTor
06327cc7be
documentation: Minor fixes 2025-05-14 09:28:41 +08:00
libtry486
b720472dc7
documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-05-14 09:28:41 +08:00
Alireza Ahmadi
96434bd18e
Fix Outbound deadlock 2025-05-14 09:28:40 +08:00
世界
caa44e0276
documentation: Fix AnyTLS doc 2025-05-14 09:28:40 +08:00
anytls
d48a81fc3d
Add AnyTLS protocol 2025-05-14 09:28:40 +08:00
世界
040abba031
Migrate to stdlib ECH support 2025-05-14 09:28:40 +08:00
世界
bbce1feed6
Add fallback local DNS server for iOS 2025-05-14 09:28:40 +08:00
世界
7ef6b84e2b
Get darwin local DNS server from libresolv 2025-05-14 09:28:39 +08:00
世界
fccd3f9e00
Improve resolve action 2025-05-14 09:28:39 +08:00
世界
a08ebe8941
Add back port hopping to hysteria 1 2025-05-14 09:28:39 +08:00
xchacha20-poly1305
161e18b479
Remove single quotes of raw Moziila certs 2025-05-14 09:28:38 +08:00
世界
d1097c23a9
Add Tailscale endpoint 2025-05-14 09:28:38 +08:00
世界
1abc3420bd
Build legacy binaries with latest Go 2025-05-14 09:28:38 +08:00
世界
226f4a3919
documentation: Remove outdated icons 2025-05-14 09:28:38 +08:00
世界
8fd9eb9736
documentation: Certificate store 2025-05-14 09:28:38 +08:00
世界
bcdd3240ad
documentation: TLS fragment 2025-05-14 09:27:47 +08:00
世界
eef461259d
documentation: Outbound domain resolver 2025-05-14 09:27:47 +08:00
世界
46e863bd6f
documentation: Refactor DNS 2025-05-14 09:27:46 +08:00
世界
ef8ebec8d1
Add certificate store 2025-05-14 09:27:46 +08:00
世界
357b4c2422
Add TLS fragment support 2025-05-14 09:27:46 +08:00
世界
414bc9841b
refactor: Outbound domain resolver 2025-05-14 09:27:46 +08:00
世界
1d100c6515
refactor: DNS 2025-05-14 09:27:43 +08:00
96 changed files with 271 additions and 3750 deletions

View File

@ -8,15 +8,11 @@
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
--no-deb-generate-changes
--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/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.fish=/usr/share/fish/vendor_completions.d/sing-box.fish

View File

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

View File

@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.24.4
go-version: ^1.24.3
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:

View File

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

View File

@ -55,12 +55,6 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/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
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

@ -136,12 +136,6 @@ nfpms:
dst: /usr/lib/systemd/system/sing-box.service
- src: release/config/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
dst: /usr/share/bash-completion/completions/sing-box.bash

View File

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

View File

@ -7,9 +7,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
"github.com/sagernet/sing/service"
"github.com/miekg/dns"
)
@ -33,30 +31,11 @@ type DNSClient interface {
}
type DNSQueryOptions struct {
Transport DNSTransport
Strategy C.DomainStrategy
LookupStrategy C.DomainStrategy
DisableCache bool
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
Transport DNSTransport
Strategy C.DomainStrategy
DisableCache bool
RewriteTTL *uint32
ClientSubnet netip.Prefix
}
type RDRCStore interface {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,6 @@
package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface {
Lifecycle
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
Start() error
Close() error
}

View File

@ -1,21 +0,0 @@
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
}

View File

@ -1,144 +0,0 @@
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

@ -1,72 +0,0 @@
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
}

View File

@ -1,18 +0,0 @@
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"
type TimeService interface {
SimpleLifecycle
Service
TimeFunc() func() time.Time
}

123
box.go
View File

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

@ -1 +1 @@
Subproject commit 320170a1077ea5c93872b3e055b96b8836615ef0
Subproject commit 9dd336679d14a1d16c6d72b1a67c4abc4ccae4c0

View File

@ -105,7 +105,7 @@ func publishTestflight(ctx context.Context) error {
return err
}
tag := tagVersion.VersionString()
client := createClient(20 * time.Minute)
client := createClient(10 * time.Minute)
log.Info(tag, " list build IDs")
buildIDsResponse, _, err := client.TestFlight.ListBuildIDsForBetaGroup(ctx, groupID, nil)
@ -145,7 +145,7 @@ func publishTestflight(ctx context.Context) error {
return err
}
build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 5*time.Minute {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
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=")
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", "with_conntrack")
iosTags = append(iosTags, "with_dhcp", "with_low_memory")
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api")
iosTags = append(iosTags, "with_dhcp", "with_low_memory", "with_conntrack")
memcTags = append(memcTags, "with_tailscale")
debugTags = append(debugTags, "debug")
}

View File

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

View File

@ -97,6 +97,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() {
if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
@ -108,10 +112,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay
}
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)

View File

@ -12,6 +12,7 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -75,11 +76,10 @@ func (c *slowOpenConn) Write(b []byte) (n int, err error) {
return c.conn.Write(b)
default:
}
conn, err := c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil {
c.err = err
} else {
c.conn = conn
c.conn = nil
c.err = E.Cause(err, "dial tcp fast open")
}
n = len(b)
close(c.create)

View File

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

View File

@ -3,11 +3,9 @@ package listener
import (
"net"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common/control"
@ -53,13 +51,6 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
}
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) {
if l.listenOptions.TCPFastOpen {
var tfoConfig tfo.ListenConfig

View File

@ -5,10 +5,8 @@ import (
"net"
"net/netip"
"os"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/redir"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
@ -38,13 +36,6 @@ func (l *Listener) ListenUDP() (net.PacketConn, error) {
if !udpFragment {
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) {
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"
)
func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
func TProxy(fd uintptr, isIPv6 bool) error {
err := syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
if err == nil {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
@ -20,13 +20,11 @@ func TProxy(fd uintptr, isIPv6 bool, isUDP bool) error {
if err == nil && isIPv6 {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
}
if isUDP {
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 {
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)
}
return err
}

View File

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

View File

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

View File

@ -11,6 +11,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
func startACME(ctx context.Context, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
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 {
config *tls.Config
logger log.Logger
acmeService adapter.SimpleLifecycle
acmeService adapter.Service
certificate []byte
key []byte
certificatePath string
@ -165,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
return nil, nil
}
var tlsConfig *tls.Config
var acmeService adapter.SimpleLifecycle
var acmeService adapter.Service
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
//nolint:staticcheck

View File

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

View File

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

View File

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

View File

@ -60,17 +60,13 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
if !serverAddr.IsValid() {
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{
TransportAdapter: adapter,
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
logger: logger,
dialer: dialer,
dialer: transportDialer,
serverAddr: serverAddr,
tlsConfig: tlsConfig,
}
}, nil
}
func (t *TLSTransport) Start(stage adapter.StartStage) error {

View File

@ -2,66 +2,10 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.22
#### 1.12.0-beta.14
* 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
* Fixes and improvements
_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)._
#### 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
* Fixes and improvements
_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)._
#### 1.12.0-beta.13
* Add TLS record fragment route options **1**

View File

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

View File

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

View File

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

View File

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

View File

@ -1,84 +0,0 @@
---
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==
The tag of the [Tailscale Endpoint](/configuration/endpoint/tailscale).
The tag of the Tailscale endpoint.
#### accept_default_resolvers
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

View File

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

View File

@ -398,11 +398,11 @@ UDP NAT 过期时间。
TCP/IP 栈。
| 栈 | 描述 |
|----------|-------------------------------------------------------------------------------------------------------|
| `system` | 基于系统网络栈执行 L3 到 L4 转换 |
| `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
| `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
| 栈 | 描述 |
|--------|------------------------------------------------------------------|
| system | 基于系统网络栈执行 L3 到 L4 转换 |
| gVisor | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
| mixed | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。

View File

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

View File

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

View File

@ -1,135 +0,0 @@
---
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

@ -1,32 +0,0 @@
---
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

@ -1,44 +0,0 @@
---
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

@ -1,52 +0,0 @@
---
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, include.ServiceRegistry())
return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry)
}
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {

16
go.mod
View File

@ -6,11 +6,9 @@ require (
github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0
github.com/cloudflare/circl v1.6.1
github.com/coder/websocket v1.8.12
github.com/cretz/bine v0.2.0
github.com/go-chi/chi/v5 v5.2.1
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/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/libdns/alidns v1.0.4-libdns.v1.beta1
@ -27,15 +25,15 @@ require (
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.6
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b
github.com/sagernet/quic-go v0.51.0-beta.5
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f
github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.5.0-beta.2
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8
github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-mod.5
github.com/sagernet/wireguard-go v0.0.1-beta.7
@ -67,6 +65,7 @@ require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
@ -79,6 +78,7 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // 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/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect

24
go.sum
View File

@ -165,25 +165,25 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/quic-go v0.51.0-beta.5 h1:/mME3sJvQ8k/JKP0oC/9XoWrm0znO7hWXviB5yiipJY=
github.com/sagernet/quic-go v0.51.0-beta.5/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b h1:ZjTCYPb5f7aHdf1UpUvE22dVmf7BL8eQ/zLZhjgh7Wo=
github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f h1:lttLhNtFuMItQcTD29QP6aBS8kR1UhG7zZ+pwzTYkFM=
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.2 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.0-beta.2 h1:j7KAbBuGmsKwSxVAQL5soJ+wDqxim4/llK2kxB0hSKk=
github.com/sagernet/sing-quic v0.5.0-beta.2/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3 h1:1J+s1yyZ8+YAYaClI+az8YuFgV9NGXUUCZnriKmos6w=
github.com/sagernet/sing-quic v0.4.1-0.20250511050139-d459f561c9c3/go.mod h1:Mv7CdSyLepmqoLT8rd88Qn3QMv5AbsgjEm3DvEhDVNE=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI=
github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 h1:zW+zAOCxUIqBCgnZiPovt1uQ3S+zBS+w0NGp+1zITGA=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=

View File

@ -3,12 +3,10 @@ package include
import (
"context"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
"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/dns/transport"
@ -35,15 +33,9 @@ import (
"github.com/sagernet/sing-box/protocol/tun"
"github.com/sagernet/sing-box/protocol/vless"
"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"
)
func Context(ctx context.Context) context.Context {
return box.Context(ctx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
}
func InboundRegistry() *inbound.Registry {
registry := inbound.NewRegistry()
@ -118,7 +110,6 @@ func DNSTransportRegistry() *dns.TransportRegistry {
hosts.RegisterTransport(registry)
local.RegisterTransport(registry)
fakeip.RegisterTransport(registry)
resolved.RegisterTransport(registry)
registerQUICTransports(registry)
registerDHCPTransport(registry)
@ -127,17 +118,6 @@ func DNSTransportRegistry() *dns.TransportRegistry {
return registry
}
func ServiceRegistry() *service.Registry {
registry := service.NewRegistry()
resolved.RegisterService(registry)
ssmapi.RegisterService(registry)
registerDERPService(registry)
return 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) {
return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

View File

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

View File

@ -7,7 +7,6 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"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"
@ -26,9 +25,3 @@ func registerTailscaleTransport(registry *dns.TransportRegistry) {
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,7 +94,6 @@ nav:
- DHCP: configuration/dns/server/dhcp.md
- FakeIP: configuration/dns/server/fakeip.md
- Tailscale: configuration/dns/server/tailscale.md
- Resolved: configuration/dns/server/resolved.md
- DNS Rule: configuration/dns/rule.md
- DNS Rule Action: configuration/dns/rule_action.md
- FakeIP: configuration/dns/fakeip.md
@ -170,11 +169,6 @@ nav:
- DNS: configuration/outbound/dns.md
- Selector: configuration/outbound/selector.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:
- pymdownx.inlinehilite
- pymdownx.snippets

View File

@ -121,6 +121,7 @@ type LegacyDNSFakeIPOptions struct {
type DNSTransportOptionsRegistry interface {
CreateOptions(transportType string) (any, bool)
}
type _DNSServerOptions struct {
Type string `json:"type,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)
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)
if !loaded {
return E.New("unknown endpoint type: ", h.Type)
return E.New("unknown inbound type: ", h.Type)
}
err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
if err != nil {

View File

@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
}
registry := service.FromContext[InboundOptionsRegistry](ctx)
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)
if !loaded {

View File

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

View File

@ -1,49 +0,0 @@
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

@ -1,47 +0,0 @@
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,7 +8,6 @@ type ShadowsocksInboundOptions struct {
Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
Multiplex *InboundMultiplexOptions `json:"multiplex,omitempty"`
Managed bool `json:"managed,omitempty"`
}
type ShadowsocksUser struct {

View File

@ -1,11 +0,0 @@
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,12 +2,6 @@ package option
import (
"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 {
@ -29,88 +23,3 @@ type TailscaleDNSServerOptions struct {
Endpoint string `json:"endpoint,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,6 +4,7 @@ import (
"context"
"net"
"net/netip"
"syscall"
"time"
"github.com/sagernet/sing-box/adapter"
@ -16,6 +17,7 @@ import (
"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/udpnat2"
@ -55,7 +57,6 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
Listen: options.ListenOptions,
ConnectionHandler: tproxy,
OOBPacketHandler: tproxy,
TProxy: true,
})
return tproxy, nil
}
@ -64,7 +65,27 @@ func (t *TProxy) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
return t.listener.Start()
err := 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 {
@ -133,10 +154,10 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks
return err
}
}
var listenConfig net.ListenConfig
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
listenConfig.Control = control.Append(listenConfig.Control, redir.TProxyWriteBack())
packetConn, err := w.listener.ListenPacket(listenConfig, w.ctx, "udp", destination.String())
var listener net.ListenConfig
listener.Control = control.Append(listener.Control, control.ReuseAddr())
listener.Control = control.Append(listener.Control, redir.TProxyWriteBack())
packetConn, err := w.listener.ListenPacket(listener, w.ctx, "udp", destination.String())
if err != nil {
return err
}

View File

@ -32,10 +32,8 @@ func RegisterInbound(registry *inbound.Registry) {
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 {
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 || options.Managed {
if len(options.Users) > 0 {
return newMultiInbound(ctx, router, logger, tag, options)
} else if len(options.Destinations) > 0 {
return newRelayInbound(ctx, router, logger, tag, options)

View File

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

View File

@ -221,14 +221,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
}
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)
udpForwarder := tun.NewUDPForwarder(t.ctx, ipStack, t, t.udpTimeout)
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)

View File

@ -214,6 +214,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded {
return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet)
}
ruleSet.IncRef()
inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet)
}
for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet {
@ -221,6 +222,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
if !loaded {
return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet)
}
ruleSet.IncRef()
inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet)
}
if options.AutoRedirect {
@ -310,7 +312,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name())
}
routeRuleSet.IncRef()
routeRuleSet.DecRef()
t.routeAddressSet = append(t.routeAddressSet, ipSets...)
if t.autoRedirect != nil {
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
@ -322,7 +324,7 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
if len(ipSets) == 0 {
t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name())
}
routeExcludeRuleSet.IncRef()
routeExcludeRuleSet.DecRef()
t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...)
if t.autoRedirect != nil {
t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet))

View File

@ -1,15 +0,0 @@
<!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

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

View File

@ -1,8 +0,0 @@
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,8 +4,6 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target
[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
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

View File

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

View File

@ -4,8 +4,6 @@ Documentation=https://sing-box.sagernet.org
After=network.target nss-lookup.target network-online.target
[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
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

View File

@ -27,16 +27,12 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad
conn.SetReadDeadline(time.Now().Add(C.DNSTimeout))
err := dnsOutbound.HandleStreamDNSRequest(ctx, r.dns, conn, metadata)
if err != nil {
if !E.IsClosedOrCanceled(err) {
return err
} else {
return nil
}
return err
}
}
}
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) {
if natConn, isNatConn := conn.(udpnat.Conn); isNatConn {
metadata.Destination = M.Socksaddr{}
for _, packet := range packetBuffers {
@ -51,22 +47,19 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
conn: conn,
ctx: ctx,
metadata: metadata,
onClose: onClose,
})
return nil
return
}
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) {
return E.Cause(err, "process DNS packet")
r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
}
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) {
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) {
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet connection"))
}
}
@ -95,16 +88,8 @@ type dnsHijacker struct {
conn N.PacketConn
ctx context.Context
metadata adapter.InboundContext
onClose N.CloseHandlerFunc
}
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination)
}
func (h *dnsHijacker) Close() error {
if h.onClose != nil {
h.onClose(nil)
}
return nil
}

View File

@ -6,6 +6,7 @@ import (
"net"
"net/netip"
"os"
"os/user"
"strings"
"time"
@ -112,12 +113,13 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
}
case *rule.RuleActionReject:
buf.ReleaseMulti(buffers)
return action.Error(ctx)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil
case *rule.RuleActionHijackDNS:
for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer)
}
N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
r.hijackDNSStream(ctx, conn, metadata)
return nil
}
}
@ -173,6 +175,8 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
} else {
r.logger.ErrorContext(ctx, err)
}
} else if onClose != nil {
onClose(nil)
}
}
@ -227,9 +231,11 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
}
case *rule.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers)
return action.Error(ctx)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx))
return nil
case *rule.RuleActionHijackDNS:
return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata)
return nil
}
}
if selectedRule == nil || selectReturn {
@ -292,16 +298,16 @@ func (r *Router) matchRule(
r.logger.InfoContext(ctx, "failed to search process: ", fErr)
} else {
if 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)
}
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
} else if processInfo.PackageName != "" {
r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
} 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 != "" {
r.logger.InfoContext(ctx, "found user: ", processInfo.User)
} else {
@ -499,9 +505,7 @@ func (r *Router) actionSniff(
return
}
if inputConn != nil {
if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
return
}
sniffBuffer := buf.NewPacket()
var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 {
streamSniffers = action.StreamSniffers
@ -515,7 +519,6 @@ func (r *Router) actionSniff(
sniff.RDP,
}
}
sniffBuffer := buf.NewPacket()
err := sniff.PeekStream(
ctx,
metadata,
@ -547,26 +550,10 @@ func (r *Router) actionSniff(
sniffBuffer.Release()
}
} else if inputPacketConn != nil {
if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
return
} else if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
return
}
var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
}
for {
var (
sniffBuffer = buf.NewPacket()
@ -606,6 +593,20 @@ func (r *Router) actionSniff(
sniff.QUICClientHello,
)
} else {
var packetSniffers []sniff.PacketSniffer
if len(action.PacketSniffers) > 0 {
packetSniffers = action.PacketSniffers
} else {
packetSniffers = []sniff.PacketSniffer{
sniff.DomainNameQuery,
sniff.QUICClientHello,
sniff.STUNMessage,
sniff.UTP,
sniff.UDPTracker,
sniff.DTLSRecord,
sniff.NTP,
}
}
err = sniff.PeekPacket(
ctx, metadata,
sniffBuffer.Bytes(),

View File

@ -1,519 +0,0 @@
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

@ -1,648 +0,0 @@
//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)
}

View File

@ -1,252 +0,0 @@
//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")
}

View File

@ -1,27 +0,0 @@
//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

@ -1,297 +0,0 @@
//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...)
}
}
}
}

View File

@ -1,181 +0,0 @@
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,
})
}

View File

@ -1,117 +0,0 @@
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,
)
}

View File

@ -1,215 +0,0 @@
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)
}
}

View File

@ -1,85 +0,0 @@
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
func init() {
globalCtx = include.Context(context.Background())
globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
}
func startInstance(t *testing.T, options option.Options) *box.Box {

View File

@ -47,7 +47,6 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
server := &Server{
ctx: ctx,
tlsConfig: tlsConfig,
logger: logger,
handler: handler,
h2Server: &http2.Server{
IdleTimeout: time.Duration(options.IdleTimeout),

View File

@ -3,7 +3,6 @@ package v2raywebsocket
import (
"context"
"encoding/base64"
"errors"
"io"
"net"
"os"
@ -68,10 +67,9 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
return
}
if !E.IsMulti(err, io.EOF, wsutil.ErrNoFrameAdvance) {
err = wrapWsError(err)
return
}
header, err = wrapWsError0(c.reader.NextFrame())
header, err = c.reader.NextFrame()
if err != nil {
return
}
@ -80,14 +78,14 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
err = wsutil.ErrFrameTooLarge
return
}
err = wrapWsError(c.controlHandler(header, c.reader))
err = c.controlHandler(header, c.reader)
if err != nil {
return
}
continue
}
if header.OpCode&ws.OpBinary == 0 {
err = wrapWsError(c.reader.Discard())
err = c.reader.Discard()
if err != nil {
return
}
@ -97,7 +95,7 @@ func (c *WebsocketConn) Read(b []byte) (n int, err error) {
}
func (c *WebsocketConn) Write(p []byte) (n int, err error) {
err = wrapWsError(wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p))
err = wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, p)
if err != nil {
return
}
@ -148,7 +146,7 @@ func (c *EarlyWebsocketConn) Read(b []byte) (n int, err error) {
return 0, c.err
}
}
return wrapWsError0(c.conn.Read(b))
return c.conn.Read(b)
}
func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
@ -193,7 +191,7 @@ func (c *EarlyWebsocketConn) writeRequest(content []byte) error {
func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
if c.conn != nil {
return wrapWsError0(c.conn.Write(b))
return c.conn.Write(b)
}
c.access.Lock()
defer c.access.Unlock()
@ -201,7 +199,7 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
return 0, c.err
}
if c.conn != nil {
return wrapWsError0(c.conn.Write(b))
return c.conn.Write(b)
}
err = c.writeRequest(b)
c.err = err
@ -214,12 +212,12 @@ func (c *EarlyWebsocketConn) Write(b []byte) (n int, err error) {
func (c *EarlyWebsocketConn) WriteBuffer(buffer *buf.Buffer) error {
if c.conn != nil {
return wrapWsError(c.conn.WriteBuffer(buffer))
return c.conn.WriteBuffer(buffer)
}
c.access.Lock()
defer c.access.Unlock()
if c.conn != nil {
return wrapWsError(c.conn.WriteBuffer(buffer))
return c.conn.WriteBuffer(buffer)
}
if c.err != nil {
return c.err
@ -274,23 +272,3 @@ func (c *EarlyWebsocketConn) Upstream() any {
func (c *EarlyWebsocketConn) LazyHeadroom() bool {
return c.conn == nil
}
func wrapWsError(err error) error {
if err == nil {
return nil
}
var closedErr wsutil.ClosedError
if errors.As(err, &closedErr) {
if closedErr.Code == ws.StatusNormalClosure {
err = io.EOF
}
}
return err
}
func wrapWsError0[T any](value T, err error) (T, error) {
if err == nil {
return value, nil
}
return value, wrapWsError(err)
}

View File

@ -0,0 +1,22 @@
package v2raywebsocket
import (
"net"
"time"
)
type deadConn struct {
net.Conn
}
func (c *deadConn) SetDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetReadDeadline(t time.Time) error {
return nil
}
func (c *deadConn) SetWriteDeadline(t time.Time) error {
return nil
}

View File

@ -66,7 +66,7 @@ func (w *Writer) WriteBuffer(buffer *buf.Buffer) error {
ws.Cipher(data, *(*[4]byte)(header[1+payloadBitLength:]), 0)
}
return wrapWsError(w.writer.WriteBuffer(buffer))
return w.writer.WriteBuffer(buffer)
}
func (w *Writer) FrontHeadroom() int {