mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-08 19:54:12 +08:00
Compare commits
118 Commits
v1.12.0-be
...
dev-next
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c3cc010880 | ||
![]() |
1920c191be | ||
![]() |
e0ac459204 | ||
![]() |
09fb897805 | ||
![]() |
a1b3d891a3 | ||
![]() |
d866a40469 | ||
![]() |
45cd04b07e | ||
![]() |
2cf0528c4d | ||
![]() |
905a2ded93 | ||
![]() |
cb3c0829c5 | ||
![]() |
1a8f6e053d | ||
![]() |
99a09a6ce5 | ||
![]() |
01b4c7fcdd | ||
![]() |
fe89f946c1 | ||
![]() |
6c17c7a8f5 | ||
![]() |
ea067e5478 | ||
![]() |
75af9a824e | ||
![]() |
a5d4a42119 | ||
![]() |
9821fbc3e3 | ||
![]() |
c0408ad1de | ||
![]() |
6b0e861afa | ||
![]() |
e32d686d6c | ||
![]() |
844308e128 | ||
![]() |
93c14db281 | ||
![]() |
b893a27dfc | ||
![]() |
d39960fa23 | ||
![]() |
ba0badd4bf | ||
![]() |
cfbb5d63d5 | ||
![]() |
8447a3edfe | ||
![]() |
1a9747a531 | ||
![]() |
583ecbea3b | ||
![]() |
bb6c8535a5 | ||
![]() |
10d90e4acc | ||
![]() |
e625012219 | ||
![]() |
670863fd5b | ||
![]() |
f7cf87142f | ||
![]() |
2597a68a01 | ||
![]() |
7354332daa | ||
![]() |
a0d382fc4e | ||
![]() |
a6da8b6654 | ||
![]() |
7385616cca | ||
![]() |
4b6784b446 | ||
![]() |
68579bb93b | ||
![]() |
6aace7b1b7 | ||
![]() |
148234b742 | ||
![]() |
97b7a451be | ||
![]() |
73b67e0b48 | ||
![]() |
88b4d04d59 | ||
![]() |
d1ec6c6dd2 | ||
![]() |
523825336a | ||
![]() |
032565a026 | ||
![]() |
aeea24ae30 | ||
![]() |
af22549f1a | ||
![]() |
57b17ceb4b | ||
![]() |
3dd308e7c3 | ||
![]() |
7f75195d86 | ||
![]() |
2fe4cad905 | ||
![]() |
f55eb75a53 | ||
![]() |
5ffb5b6ad2 | ||
![]() |
a1d5931759 | ||
![]() |
9e68e909cb | ||
![]() |
117e8b76cc | ||
![]() |
d2f83bfd50 | ||
![]() |
eaef13febe | ||
![]() |
0110c69dc9 | ||
![]() |
fb2f5af1fb | ||
![]() |
1553923118 | ||
![]() |
0ada49489d | ||
![]() |
95d5ca9393 | ||
![]() |
6cebbb4590 | ||
![]() |
0ef81bb5ef | ||
![]() |
0d30a1df9d | ||
![]() |
563499d2f9 | ||
![]() |
f10c0c1c8d | ||
![]() |
428074d88b | ||
![]() |
fa18832ad2 | ||
![]() |
87bce2de29 | ||
![]() |
f5020554e4 | ||
![]() |
31f3623b8a | ||
![]() |
bb42657177 | ||
![]() |
f19ff7eca7 | ||
![]() |
8e45133f2e | ||
![]() |
63df88675f | ||
![]() |
0423244298 | ||
![]() |
a5f1af9587 | ||
![]() |
112817c1a4 | ||
![]() |
6e91de51f1 | ||
![]() |
efc5c542fb | ||
![]() |
f1b569c7d1 | ||
![]() |
a752197d5e | ||
![]() |
65517d4513 | ||
![]() |
ccf4fa4d3a | ||
![]() |
18dbb823a1 | ||
![]() |
4ec058e91a | ||
![]() |
6eed06b2c2 | ||
![]() |
dd209cc9d5 | ||
![]() |
b0c0a6b07d | ||
![]() |
951a8fabbf | ||
![]() |
928298b528 | ||
![]() |
5b84fa0137 | ||
![]() |
2bb85ac8a1 | ||
![]() |
43a9016c83 | ||
![]() |
255068fd40 | ||
![]() |
098a00b025 | ||
![]() |
dba0b5276b | ||
![]() |
78ae935468 | ||
![]() |
3ea5f76470 | ||
![]() |
b4d294c05e | ||
![]() |
83cf5f5c6a | ||
![]() |
e7b3a8eebe | ||
![]() |
ee3a42a67e | ||
![]() |
50227c0f5f | ||
![]() |
bc5eb1e1a5 | ||
![]() |
995267a042 | ||
![]() |
41226a6075 | ||
![]() |
81d32181ce | ||
![]() |
c5ecca3938 | ||
![]() |
900888731c |
@ -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
|
||||||
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@ -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: |
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
5
Makefile
5
Makefile
@ -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)"
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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(), "]")
|
||||||
|
@ -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 (
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
21
adapter/service/adapter.go
Normal file
21
adapter/service/adapter.go
Normal 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
144
adapter/service/manager.go
Normal 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
|
||||||
|
}
|
72
adapter/service/registry.go
Normal file
72
adapter/service/registry.go
Normal 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
18
adapter/ssm.go
Normal 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
|
||||||
|
}
|
@ -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
123
box.go
@ -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
|
@ -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
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
@ -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())
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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":
|
||||||
|
@ -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`)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
32
common/tlsfragment/conn_test.go
Normal file
32
common/tlsfragment/conn_test.go
Normal 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())
|
||||||
|
}
|
@ -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 (
|
||||||
|
@ -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},
|
||||||
}
|
}
|
||||||
|
@ -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>"))
|
||||||
|
@ -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 {
|
||||||
|
@ -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**
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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.
|
||||||
|
@ -81,7 +81,7 @@ icon: material/new-box
|
|||||||
|
|
||||||
#### method
|
#### method
|
||||||
|
|
||||||
- `default`: 返回 NXDOMAIN。
|
- `default`: 返回 REFUSED。
|
||||||
- `drop`: 丢弃请求。
|
- `drop`: 丢弃请求。
|
||||||
|
|
||||||
默认使用 `defualt`。
|
默认使用 `defualt`。
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
84
docs/configuration/dns/server/resolved.md
Normal file
84
docs/configuration/dns/server/resolved.md
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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` 栈。
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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/) |
|
||||||
|
|
||||||
### 检查
|
### 检查
|
||||||
|
@ -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
|
||||||
|
@ -6,6 +6,10 @@ 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.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
|
||||||
|
135
docs/configuration/service/derp.md
Normal file
135
docs/configuration/service/derp.md
Normal 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__ }
|
||||||
|
```
|
32
docs/configuration/service/index.md
Normal file
32
docs/configuration/service/index.md
Normal 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.
|
44
docs/configuration/service/resolved.md
Normal file
44
docs/configuration/service/resolved.md
Normal 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.
|
52
docs/configuration/service/ssm-api.md
Normal file
52
docs/configuration/service/ssm-api.md
Normal 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).
|
@ -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
40
go.mod
@ -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
68
go.sum
@ -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=
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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"`
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
49
option/resolved.go
Normal 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"`
|
||||||
|
}
|
@ -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
47
option/service.go
Normal 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
|
||||||
|
}
|
@ -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
11
option/ssmapi.go
Normal 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
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
15
release/config/sing-box-split-dns.xml
Normal file
15
release/config/sing-box-split-dns.xml
Normal 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>
|
3
release/config/sing-box.postinst
Normal file
3
release/config/sing-box.postinst
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
systemd-sysusers sing-box.conf
|
8
release/config/sing-box.rules
Normal file
8
release/config/sing-box.rules
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
@ -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
|
||||||
|
1
release/config/sing-box.sysusers
Normal file
1
release/config/sing-box.sysusers
Normal file
@ -0,0 +1 @@
|
|||||||
|
u sing-box - "sing-box Service"
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
25
route/dns.go
25
route/dns.go
@ -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
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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(),
|
||||||
|
@ -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
519
service/derp/service.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
648
service/resolved/resolve1.go
Normal file
648
service/resolved/resolve1.go
Normal 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
252
service/resolved/service.go
Normal 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
27
service/resolved/stub.go
Normal 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")
|
||||||
|
})
|
||||||
|
}
|
297
service/resolved/transport.go
Normal file
297
service/resolved/transport.go
Normal 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
181
service/ssmapi/api.go
Normal 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
117
service/ssmapi/server.go
Normal 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
215
service/ssmapi/traffic.go
Normal 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
Loading…
x
Reference in New Issue
Block a user