Compare commits

..

118 Commits

Author SHA1 Message Date
世界
c3cc010880
documentation: Bump version 2025-06-06 23:17:46 +08:00
世界
1920c191be
Fix systemd package 2025-06-06 23:17:46 +08:00
世界
e0ac459204
Fix missing home for derp service 2025-06-06 23:17:46 +08:00
Zero Clover
09fb897805
documentation: Fix services 2025-06-06 23:17:46 +08:00
世界
a1b3d891a3
Fix dns.client_subnet ignored 2025-06-06 23:17:46 +08:00
世界
d866a40469
documentation: Minor fixes 2025-06-06 23:17:46 +08:00
世界
45cd04b07e
Fix tailscale forward 2025-06-06 23:17:46 +08:00
世界
2cf0528c4d
Minor fixes 2025-06-06 23:17:46 +08:00
世界
905a2ded93
Add SSM API service 2025-06-06 23:17:46 +08:00
世界
cb3c0829c5
Add resolved service and DNS server 2025-06-06 23:17:45 +08:00
世界
1a8f6e053d
Add DERP service 2025-06-06 23:17:27 +08:00
世界
99a09a6ce5
Add service component type 2025-06-06 23:17:27 +08:00
世界
01b4c7fcdd
Fix tproxy tcp control 2025-06-06 23:17:27 +08:00
愚者
fe89f946c1
release: Fix build tags for android
Signed-off-by: 愚者 <11926619+FansChou@users.noreply.github.com>
2025-06-06 23:17:27 +08:00
世界
6c17c7a8f5
prevent creation of bind and mark controls on unsupported platforms 2025-06-06 23:17:27 +08:00
PuerNya
ea067e5478
documentation: Fix description of reject DNS action behavior 2025-06-06 23:17:27 +08:00
Restia-Ashbell
75af9a824e
Fix TLS record fragment 2025-06-06 23:17:27 +08:00
世界
a5d4a42119
Add missing accept_routes option for Tailscale 2025-06-06 23:17:26 +08:00
世界
9821fbc3e3
Add TLS record fragment support 2025-06-06 23:17:26 +08:00
世界
c0408ad1de
release: Update Go to 1.24.3 2025-06-06 23:17:26 +08:00
世界
6b0e861afa
Fix set edns0 client subnet 2025-06-06 23:17:26 +08:00
世界
e32d686d6c
Update minor dependencies 2025-06-06 23:17:26 +08:00
世界
844308e128
Update certmagic and providers 2025-06-06 23:17:26 +08:00
世界
93c14db281
Update protobuf and grpc 2025-06-06 23:17:26 +08:00
世界
b893a27dfc
Add control options for listeners 2025-06-06 23:17:25 +08:00
世界
d39960fa23
Update quic-go to v0.52.0 2025-06-06 23:17:25 +08:00
世界
ba0badd4bf
Update utls to v1.7.2 2025-06-06 23:17:25 +08:00
世界
cfbb5d63d5
Handle EDNS version downgrade 2025-06-06 23:16:21 +08:00
世界
8447a3edfe
documentation: Fix anytls padding scheme description 2025-06-06 23:16:21 +08:00
安容
1a9747a531
Report invalid DNS address early 2025-06-06 23:16:20 +08:00
世界
583ecbea3b
Fix wireguard listen_port 2025-06-06 23:16:20 +08:00
世界
bb6c8535a5
clash-api: Add more meta api 2025-06-06 23:16:19 +08:00
世界
10d90e4acc
Fix DNS lookup 2025-06-06 23:16:19 +08:00
世界
e625012219
Fix fetch ECH configs 2025-06-06 23:16:19 +08:00
reletor
670863fd5b
documentation: Minor fixes 2025-06-06 23:16:19 +08:00
caelansar
f7cf87142f
Fix callback deletion in UDP transport 2025-06-06 23:16:18 +08:00
世界
2597a68a01
documentation: Try to make the play review happy 2025-06-06 23:16:18 +08:00
世界
7354332daa
Fix missing handling of legacy domain_strategy options 2025-06-06 23:16:18 +08:00
世界
a0d382fc4e
Improve local DNS server 2025-06-06 23:16:18 +08:00
anytls
a6da8b6654
Update anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:17 +08:00
世界
7385616cca
Fix DNS dialer 2025-06-06 23:16:17 +08:00
世界
4b6784b446
release: Skip override version for iOS 2025-06-06 23:16:16 +08:00
iikira
68579bb93b
Fix UDP DNS server crash
Signed-off-by: iikira <i2@mail.iikira.com>
2025-06-06 23:16:16 +08:00
ReleTor
6aace7b1b7
Fix fetch ECH configs 2025-06-06 23:16:16 +08:00
世界
148234b742
Allow direct outbounds without domain_resolver 2025-06-06 23:16:16 +08:00
世界
97b7a451be
Fix Tailscale dialer 2025-06-06 23:16:15 +08:00
dyhkwong
73b67e0b48
Fix DNS over QUIC stream close 2025-06-06 23:16:15 +08:00
anytls
88b4d04d59
Update anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:15 +08:00
Rambling2076
d1ec6c6dd2
Fix missing with_tailscale in Dockerfile
Signed-off-by: Rambling2076 <Rambling2076@proton.me>
2025-06-06 23:16:14 +08:00
世界
523825336a
Fail when default DNS server not found 2025-06-06 23:16:14 +08:00
世界
032565a026
Update gVisor to 20250319.0 2025-06-06 23:16:14 +08:00
世界
aeea24ae30
Explicitly reject detour to empty direct outbounds 2025-06-06 23:16:14 +08:00
世界
af22549f1a
Add netns support 2025-06-06 23:16:14 +08:00
世界
57b17ceb4b
Add wildcard name support for predefined records 2025-06-06 23:16:13 +08:00
世界
3dd308e7c3
Remove map usage in options 2025-06-06 23:16:13 +08:00
世界
7f75195d86
Fix unhandled DNS loop 2025-06-06 23:16:13 +08:00
世界
2fe4cad905
Add wildcard-sni support for shadow-tls inbound 2025-06-06 23:16:12 +08:00
k9982874
f55eb75a53
Add ntp protocol sniffing 2025-06-06 23:16:12 +08:00
世界
5ffb5b6ad2
option: Fix marshal legacy DNS options 2025-06-06 23:16:12 +08:00
世界
a1d5931759
Make domain_resolver optional when only one DNS server is configured 2025-06-06 23:16:12 +08:00
世界
9e68e909cb
Fix DNS lookup context pollution 2025-06-06 23:16:11 +08:00
世界
117e8b76cc
Fix http3 DNS server connecting to wrong address 2025-06-06 23:16:11 +08:00
Restia-Ashbell
d2f83bfd50
documentation: Fix typo 2025-06-06 23:16:11 +08:00
anytls
eaef13febe
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:11 +08:00
k9982874
0110c69dc9
Fix hosts DNS server 2025-06-06 23:16:10 +08:00
世界
fb2f5af1fb
Fix UDP DNS server crash 2025-06-06 23:16:10 +08:00
世界
1553923118
documentation: Fix missing ip_accept_any DNS rule option 2025-06-06 23:16:10 +08:00
世界
0ada49489d
Fix anytls dialer usage 2025-06-06 23:16:10 +08:00
世界
95d5ca9393
Move predefined DNS server to rule action 2025-06-06 23:16:10 +08:00
世界
6cebbb4590
Fix domain resolver on direct outbound 2025-06-06 23:16:09 +08:00
Zephyruso
0ef81bb5ef
Fix missing AnyTLS display name 2025-06-06 23:16:09 +08:00
anytls
0d30a1df9d
Update sing-anytls
Co-authored-by: anytls <anytls>
2025-06-06 23:16:09 +08:00
Estel
563499d2f9
documentation: Fix typo
Signed-off-by: Estel <callmebedrockdigger@gmail.com>
2025-06-06 23:16:08 +08:00
TargetLocked
f10c0c1c8d
Fix parsing legacy DNS options 2025-06-06 23:16:08 +08:00
世界
428074d88b
Fix DNS fallback 2025-06-06 23:16:07 +08:00
世界
fa18832ad2
documentation: Fix missing hosts DNS server 2025-06-06 23:16:07 +08:00
anytls
87bce2de29
Add MinIdleSession option to AnyTLS outbound
Co-authored-by: anytls <anytls>
2025-06-06 23:16:06 +08:00
ReleTor
f5020554e4
documentation: Minor fixes 2025-06-06 23:16:06 +08:00
libtry486
31f3623b8a
documentation: Fix typo
fix typo

Signed-off-by: libtry486 <89328481+libtry486@users.noreply.github.com>
2025-06-06 23:16:05 +08:00
Alireza Ahmadi
bb42657177
Fix Outbound deadlock 2025-06-06 23:16:05 +08:00
世界
f19ff7eca7
documentation: Fix AnyTLS doc 2025-06-06 23:16:05 +08:00
anytls
8e45133f2e
Add AnyTLS protocol 2025-06-06 23:16:04 +08:00
世界
63df88675f
Migrate to stdlib ECH support 2025-06-06 23:16:04 +08:00
世界
0423244298
Add fallback local DNS server for iOS 2025-06-06 23:16:03 +08:00
世界
a5f1af9587
Get darwin local DNS server from libresolv 2025-06-06 23:16:03 +08:00
世界
112817c1a4
Improve resolve action 2025-06-06 23:16:02 +08:00
世界
6e91de51f1
Add back port hopping to hysteria 1 2025-06-06 23:16:02 +08:00
xchacha20-poly1305
efc5c542fb
Remove single quotes of raw Moziila certs 2025-06-06 23:16:02 +08:00
世界
f1b569c7d1
Add Tailscale endpoint 2025-06-06 23:16:02 +08:00
世界
a752197d5e
Build legacy binaries with latest Go 2025-06-06 23:16:01 +08:00
世界
65517d4513
documentation: Remove outdated icons 2025-06-06 23:16:01 +08:00
世界
ccf4fa4d3a
documentation: Certificate store 2025-06-06 23:16:01 +08:00
世界
18dbb823a1
documentation: TLS fragment 2025-06-06 23:16:01 +08:00
世界
4ec058e91a
documentation: Outbound domain resolver 2025-06-06 23:16:01 +08:00
世界
6eed06b2c2
documentation: Refactor DNS 2025-06-06 23:16:00 +08:00
世界
dd209cc9d5
Add certificate store 2025-06-06 23:16:00 +08:00
世界
b0c0a6b07d
Add TLS fragment support 2025-06-06 23:15:59 +08:00
世界
951a8fabbf
refactor: Outbound domain resolver 2025-06-06 23:15:59 +08:00
世界
928298b528
refactor: DNS 2025-06-06 23:15:59 +08:00
世界
5b84fa0137
Fix default network strategy 2025-06-06 14:50:38 +08:00
世界
2bb85ac8a1
Fix slowOpenConn 2025-06-06 14:39:40 +08:00
世界
43a9016c83
Fix leak in hijack-dns 2025-06-06 14:28:09 +08:00
世界
255068fd40
Bump version 2025-06-04 23:32:10 +08:00
世界
098a00b025
Fix v2ray websocket transport 2025-06-04 23:23:36 +08:00
世界
dba0b5276b
Bump version 2025-06-04 20:06:38 +08:00
Sentsuki
78ae935468
documentation: Fix typo
Signed-off-by: Sentsuki <52487960+Sentsuki@users.noreply.github.com>
2025-06-04 20:06:38 +08:00
Mahdi
3ea5f76470
Fix nil logger at v2rayhttp server 2025-06-04 20:06:20 +08:00
世界
b4d294c05e
Fix TUIC read buffer 2025-06-04 20:03:51 +08:00
世界
83cf5f5c6a
Fix ws closed error message 2025-05-27 14:30:07 +08:00
世界
e7b3a8eebe
Fix vmess read request 2025-05-27 14:11:05 +08:00
世界
ee3a42a67e
Fix none method read buffer 2025-05-27 14:03:48 +08:00
世界
50227c0f5f
Fix sniff action 2025-05-26 18:24:35 +08:00
世界
bc5eb1e1a5
Fix RoutePacketConnectionEx 2025-05-24 08:14:43 +08:00
世界
995267a042
Remove wrong ALPNs in DOH/DOH3 2025-05-24 08:00:13 +08:00
世界
41226a6075
Fix interface finder 2025-05-23 10:57:38 +08:00
世界
81d32181ce
Fix update route address set 2025-05-20 19:46:54 +08:00
世界
c5ecca3938
Bump version 2025-05-18 16:48:44 +08:00
世界
900888731c
Fix DNS reject response 2025-05-13 18:05:31 +08:00
106 changed files with 3962 additions and 337 deletions

View File

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

View File

@ -80,7 +80,7 @@ jobs:
- name: Set build tags - name: Set build tags
run: | run: |
set -xeuo pipefail set -xeuo pipefail
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api' TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}" echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
- name: Build - name: Build
run: | run: |

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,27 @@
package adapter package adapter
import (
"context"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
type Service interface { type Service interface {
Start() error Lifecycle
Close() error Type() string
Tag() string
}
type ServiceRegistry interface {
option.ServiceOptionsRegistry
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
}
type ServiceManager interface {
Lifecycle
Services() []Service
Get(tag string) (Service, bool)
Remove(tag string) error
Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
} }

View File

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

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

@ -0,0 +1,144 @@
package service
import (
"context"
"os"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/taskmonitor"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.ServiceManager = (*Manager)(nil)
type Manager struct {
logger log.ContextLogger
registry adapter.ServiceRegistry
access sync.Mutex
started bool
stage adapter.StartStage
services []adapter.Service
serviceByTag map[string]adapter.Service
}
func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
return &Manager{
logger: logger,
registry: registry,
serviceByTag: make(map[string]adapter.Service),
}
}
func (m *Manager) Start(stage adapter.StartStage) error {
m.access.Lock()
if m.started && m.stage >= stage {
panic("already started")
}
m.started = true
m.stage = stage
services := m.services
m.access.Unlock()
for _, service := range services {
err := adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
return nil
}
func (m *Manager) Close() error {
m.access.Lock()
defer m.access.Unlock()
if !m.started {
return nil
}
m.started = false
services := m.services
m.services = nil
monitor := taskmonitor.New(m.logger, C.StopTimeout)
var err error
for _, service := range services {
monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
err = E.Append(err, service.Close(), func(err error) error {
return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
})
monitor.Finish()
}
return nil
}
func (m *Manager) Services() []adapter.Service {
m.access.Lock()
defer m.access.Unlock()
return m.services
}
func (m *Manager) Get(tag string) (adapter.Service, bool) {
m.access.Lock()
service, found := m.serviceByTag[tag]
m.access.Unlock()
return service, found
}
func (m *Manager) Remove(tag string) error {
m.access.Lock()
service, found := m.serviceByTag[tag]
if !found {
m.access.Unlock()
return os.ErrInvalid
}
delete(m.serviceByTag, tag)
index := common.Index(m.services, func(it adapter.Service) bool {
return it == service
})
if index == -1 {
panic("invalid service index")
}
m.services = append(m.services[:index], m.services[index+1:]...)
started := m.started
m.access.Unlock()
if started {
return service.Close()
}
return nil
}
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
if err != nil {
return err
}
m.access.Lock()
defer m.access.Unlock()
if m.started {
for _, stage := range adapter.ListStartStages {
err = adapter.LegacyStart(service, stage)
if err != nil {
return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
}
}
}
if existsService, loaded := m.serviceByTag[tag]; loaded {
if m.started {
err = existsService.Close()
if err != nil {
return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
}
}
existsIndex := common.Index(m.services, func(it adapter.Service) bool {
return it == existsService
})
if existsIndex == -1 {
panic("invalid service index")
}
m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
}
m.services = append(m.services, service)
m.serviceByTag[tag] = service
return nil
}

View File

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

18
adapter/ssm.go Normal file
View File

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

View File

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

123
box.go
View File

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

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

View File

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

View File

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

View File

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

View File

@ -66,11 +66,17 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
interfaceFinder = control.NewDefaultInterfaceFinder() interfaceFinder = control.NewDefaultInterfaceFinder()
} }
if options.BindInterface != "" { if options.BindInterface != "" {
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
}
bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)
} }
if options.RoutingMark > 0 { if options.RoutingMark > 0 {
if !C.IsLinux {
return nil, E.New("`routing_mark` is only supported on Linux")
}
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false)) listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
} }
@ -91,10 +97,6 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
} else if networkManager.AutoDetectInterface() { } else if networkManager.AutoDetectInterface() {
if platformInterface != nil { if platformInterface != nil {
networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy)
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
networkType = common.Map(options.NetworkType, option.InterfaceType.Build) networkType = common.Map(options.NetworkType, option.InterfaceType.Build)
fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build)
if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 {
@ -106,6 +108,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 {
networkFallbackDelay = defaultOptions.FallbackDelay networkFallbackDelay = defaultOptions.FallbackDelay
} }
if networkStrategy == nil {
networkStrategy = common.Ptr(C.NetworkStrategyDefault)
defaultNetworkStrategy = true
}
bindFunc := networkManager.ProtectFunc() bindFunc := networkManager.ProtectFunc()
dialer.Control = control.Append(dialer.Control, bindFunc) dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,6 +55,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.DNSOp
DisableExpire: options.DNSClientOptions.DisableExpire, DisableExpire: options.DNSClientOptions.DisableExpire,
IndependentCache: options.DNSClientOptions.IndependentCache, IndependentCache: options.DNSClientOptions.IndependentCache,
CacheCapacity: options.DNSClientOptions.CacheCapacity, CacheCapacity: options.DNSClientOptions.CacheCapacity,
ClientSubnet: options.DNSClientOptions.ClientSubnet.Build(netip.Prefix{}),
RDRC: func() adapter.RDRCStore { RDRC: func() adapter.RDRCStore {
cacheFile := service.FromContext[adapter.CacheFile](ctx) cacheFile := service.FromContext[adapter.CacheFile](ctx)
if cacheFile == nil { if cacheFile == nil {
@ -258,7 +259,14 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { switch action.Method {
case C.RuleActionRejectMethodDefault: case C.RuleActionRejectMethodDefault:
return FixedResponse(message.Id, message.Question[0], nil, 0), nil return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop: case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop return nil, tun.ErrDrop
} }
@ -285,7 +293,12 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
} else if errors.Is(err, ErrResponseRejected) { } else if errors.Is(err, ErrResponseRejected) {
rejected = true rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String()))) r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
/*} else if responseCheck!= nil && errors.Is(err, RcodeError(mDNS.RcodeNameError)) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
*/
} else if len(message.Question) > 0 { } else if len(message.Question) > 0 {
rejected = true
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String()))) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else { } else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>")) r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))

View File

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

View File

@ -2,10 +2,80 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.0-beta.11 #### 1.12.0-beta.22
* Fixes and improvements * 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**
* Add missing `accept_routes` option for Tailscale **2**
* Fixes and improvements
**1**:
See [Route Action](/configuration/route/rule_action/#tls_record_fragment).
**2**:
See [Tailscale](/configuration/endpoint/tailscale/#accept_routes).
#### 1.12.0-beta.10 #### 1.12.0-beta.10
* Add control options for listeners **1** * Add control options for listeners **1**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

40
go.mod
View File

@ -6,18 +6,20 @@ require (
github.com/anytls/sing-anytls v0.0.8 github.com/anytls/sing-anytls v0.0.8
github.com/caddyserver/certmagic v0.23.0 github.com/caddyserver/certmagic v0.23.0
github.com/cloudflare/circl v1.6.1 github.com/cloudflare/circl v1.6.1
github.com/coder/websocket v1.8.12
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/chi/v5 v5.2.1
github.com/go-chi/render v1.0.3 github.com/go-chi/render v1.0.3
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/gofrs/uuid/v5 v5.3.2 github.com/gofrs/uuid/v5 v5.3.2
github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f
github.com/libdns/alidns v1.0.4-libdns.v1.beta1 github.com/libdns/alidns v1.0.4-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250430151523-b46a2b0885f6 github.com/libdns/cloudflare v0.2.2-0.20250430151523-b46a2b0885f6
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.7.0-alpha.2 github.com/metacubex/utls v1.7.0-alpha.3
github.com/mholt/acmez/v3 v3.1.2 github.com/mholt/acmez/v3 v3.1.2
github.com/miekg/dns v1.1.65 github.com/miekg/dns v1.1.66
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
@ -25,17 +27,17 @@ require (
github.com/sagernet/fswatch v0.1.1 github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.6 github.com/sagernet/gomobile v0.1.6
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.51.0-beta.1 github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b
github.com/sagernet/sing-mux v0.3.2 github.com/sagernet/sing-mux v0.3.2
github.com/sagernet/sing-quic v0.4.1-0.20250507070325-d2fb1cb09565 github.com/sagernet/sing-quic v0.5.0-beta.2
github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 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-tun v0.6.6-0.20250428031943-0686f8c4f210
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88
github.com/sagernet/smux v1.5.34-mod.2 github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-mod.4 github.com/sagernet/tailscale v1.80.3-mod.5
github.com/sagernet/wireguard-go v0.0.1-beta.7 github.com/sagernet/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
@ -43,11 +45,11 @@ require (
github.com/vishvananda/netns v0.0.5 github.com/vishvananda/netns v0.0.5
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.37.0 golang.org/x/crypto v0.38.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
golang.org/x/mod v0.24.0 golang.org/x/mod v0.24.0
golang.org/x/net v0.39.0 golang.org/x/net v0.40.0
golang.org/x/sys v0.32.0 golang.org/x/sys v0.33.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.72.0 google.golang.org/grpc v1.72.0
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.6
@ -65,7 +67,6 @@ require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
@ -78,7 +79,6 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
@ -123,16 +123,14 @@ require (
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/sync v0.13.0 // indirect golang.org/x/sync v0.14.0 // indirect
golang.org/x/term v0.31.0 // indirect golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.25.0 // indirect
golang.org/x/time v0.9.0 // indirect golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.32.0 // indirect golang.org/x/tools v0.33.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.3.0 // indirect lukechampine.com/blake3 v1.3.0 // indirect
) )
//replace github.com/sagernet/sing => ../sing

68
go.sum
View File

@ -125,12 +125,12 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.0-alpha.2 h1:kLRg6zDV12R1uclL5qW9Tx4RD6ztGIIrTZWY5zrJXCg= github.com/metacubex/utls v1.7.0-alpha.3 h1:cp1cEMUnoifiWrGHRzo+nCwPRveN9yPD8QaRFmfcYxA=
github.com/metacubex/utls v1.7.0-alpha.2/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/utls v1.7.0-alpha.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
@ -165,29 +165,29 @@ 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/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 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.51.0-beta.1 h1:bDMzfFlUHvMiKYvvPbOTKLWOYJFaACpssQYqsViQknI= github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.51.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.52.0-beta.1/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.9/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.11-0.20250521033217-30d675ea099b h1:ZjTCYPb5f7aHdf1UpUvE22dVmf7BL8eQ/zLZhjgh7Wo=
github.com/sagernet/sing v0.6.10-0.20250505040842-ba62fee9470f/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.11-0.20250521033217-30d675ea099b/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 h1:meZVFiiStvHThb/trcpAkCrmtJOuItG5Dzl1RRP5/NE=
github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= github.com/sagernet/sing-mux v0.3.2/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.4.1-0.20250507070325-d2fb1cb09565 h1:svvkxc3NtONBrvB2rrNVmFmFYoBv+jORDOFX6tvFDjY= github.com/sagernet/sing-quic v0.5.0-beta.2 h1:j7KAbBuGmsKwSxVAQL5soJ+wDqxim4/llK2kxB0hSKk=
github.com/sagernet/sing-quic v0.4.1-0.20250507070325-d2fb1cb09565/go.mod h1:6K3ESuaXFTjz2Dv6/PNQqg5UK0J1ZO49rqrU2ScZBKg= github.com/sagernet/sing-quic v0.5.0-beta.2/go.mod h1:SAv/qdeDN+75msGG5U5ZIwG+3Ua50jVIKNrRSY8pkx0=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.1/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 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= 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 h1:6H4BZaTqKI3YcDMyTV3E576LuJM4S4wY99xoq2T1ECw=
github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-tun v0.6.6-0.20250428031943-0686f8c4f210/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8 h1:zW+zAOCxUIqBCgnZiPovt1uQ3S+zBS+w0NGp+1zITGA= github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88 h1:0pVm8sPOel+BoiCddW3pV3cKDKEaSioVTYDdTSKjyFI=
github.com/sagernet/sing-vmess v0.2.2-0.20250503051933-9b4cf17393f8/go.mod h1:IL8Rr+EGwuqijszZkNrEFTQDKhilEpkqFqOlvdpS6/w= github.com/sagernet/sing-vmess v0.2.4-0.20250605032146-38cc72672c88/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 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc= 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.4 h1:9UgYq8m9mwX5dbTbueVxbRh+bq7AayxemJGM2PkJQnE= github.com/sagernet/tailscale v1.80.3-mod.5 h1:7V7z+p2C//TGtff20pPnDCt3qP6uFyY62peJoKF9z/A=
github.com/sagernet/tailscale v1.80.3-mod.4/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/tailscale v1.80.3-mod.5/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI= github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
@ -263,21 +263,21 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wus
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -286,20 +286,20 @@ golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

49
option/resolved.go Normal file
View File

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

View File

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

47
option/service.go Normal file
View File

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

View File

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

11
option/ssmapi.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -6,7 +6,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"os/user"
"strings" "strings"
"time" "time"
@ -113,13 +112,12 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
} }
case *rule.RuleActionReject: case *rule.RuleActionReject:
buf.ReleaseMulti(buffers) buf.ReleaseMulti(buffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
for _, buffer := range buffers { for _, buffer := range buffers {
conn = bufio.NewCachedConn(conn, buffer) conn = bufio.NewCachedConn(conn, buffer)
} }
r.hijackDNSStream(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, r.hijackDNSStream(ctx, conn, metadata))
return nil return nil
} }
} }
@ -175,8 +173,6 @@ func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn,
} else { } else {
r.logger.ErrorContext(ctx, err) r.logger.ErrorContext(ctx, err)
} }
} else if onClose != nil {
onClose(nil)
} }
} }
@ -231,11 +227,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
} }
case *rule.RuleActionReject: case *rule.RuleActionReject:
N.ReleaseMultiPacketBuffer(packetBuffers) N.ReleaseMultiPacketBuffer(packetBuffers)
N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return action.Error(ctx)
return nil
case *rule.RuleActionHijackDNS: case *rule.RuleActionHijackDNS:
r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) return r.hijackDNSPacket(ctx, conn, packetBuffers, metadata, onClose)
return nil
} }
} }
if selectedRule == nil || selectReturn { if selectedRule == nil || selectReturn {
@ -298,16 +292,16 @@ func (r *Router) matchRule(
r.logger.InfoContext(ctx, "failed to search process: ", fErr) r.logger.InfoContext(ctx, "failed to search process: ", fErr)
} else { } else {
if processInfo.ProcessPath != "" { if processInfo.ProcessPath != "" {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) if processInfo.User != "" {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user: ", processInfo.User)
} else if processInfo.UserId != -1 {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath, ", user id: ", processInfo.UserId)
} else {
r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath)
}
} else if processInfo.PackageName != "" { } else if processInfo.PackageName != "" {
r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName) r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName)
} else if processInfo.UserId != -1 { } else if processInfo.UserId != -1 {
if /*needUserName &&*/ true {
osUser, _ := user.LookupId(F.ToString(processInfo.UserId))
if osUser != nil {
processInfo.User = osUser.Username
}
}
if processInfo.User != "" { if processInfo.User != "" {
r.logger.InfoContext(ctx, "found user: ", processInfo.User) r.logger.InfoContext(ctx, "found user: ", processInfo.User)
} else { } else {
@ -452,6 +446,9 @@ match:
metadata.TLSFragment = true metadata.TLSFragment = true
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
} }
if routeOptions.TLSRecordFragment {
metadata.TLSRecordFragment = true
}
} }
switch action := currentRule.Action().(type) { switch action := currentRule.Action().(type) {
case *rule.RuleActionSniff: case *rule.RuleActionSniff:
@ -502,7 +499,9 @@ func (r *Router) actionSniff(
return return
} }
if inputConn != nil { if inputConn != nil {
sniffBuffer := buf.NewPacket() if len(action.StreamSniffers) == 0 && len(action.PacketSniffers) > 0 {
return
}
var streamSniffers []sniff.StreamSniffer var streamSniffers []sniff.StreamSniffer
if len(action.StreamSniffers) > 0 { if len(action.StreamSniffers) > 0 {
streamSniffers = action.StreamSniffers streamSniffers = action.StreamSniffers
@ -516,6 +515,7 @@ func (r *Router) actionSniff(
sniff.RDP, sniff.RDP,
} }
} }
sniffBuffer := buf.NewPacket()
err := sniff.PeekStream( err := sniff.PeekStream(
ctx, ctx,
metadata, metadata,
@ -547,10 +547,26 @@ func (r *Router) actionSniff(
sniffBuffer.Release() sniffBuffer.Release()
} }
} else if inputPacketConn != nil { } else if inputPacketConn != nil {
if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) { if len(action.PacketSniffers) == 0 && len(action.StreamSniffers) > 0 {
return
} else if metadata.PacketSniffError != nil && !errors.Is(metadata.PacketSniffError, sniff.ErrNeedMoreData) {
r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError) r.logger.DebugContext(ctx, "packet sniff skipped due to previous error: ", metadata.PacketSniffError)
return 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 { for {
var ( var (
sniffBuffer = buf.NewPacket() sniffBuffer = buf.NewPacket()
@ -590,20 +606,6 @@ func (r *Router) actionSniff(
sniff.QUICClientHello, sniff.QUICClientHello,
) )
} else { } 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( err = sniff.PeekPacket(
ctx, metadata, ctx, metadata,
sniffBuffer.Bytes(), sniffBuffer.Bytes(),

View File

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

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

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

View File

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

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

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

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

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

View File

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

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

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

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

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

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

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

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