Compare commits

...

17 Commits

Author SHA1 Message Date
renovate[bot]
9a45707eb7
[dependencies] Update golang Docker tag to v1.21 2023-08-13 16:57:27 +08:00
世界
2b4787cd5d
Fix UDP route 2023-08-13 16:57:27 +08:00
armv9
7666dba634
Fix process search with fakeip 2023-08-13 16:57:27 +08:00
世界
c4c1e1197a
platform: Add group expand status 2023-08-13 16:57:27 +08:00
世界
5f5b3e278c
Add new issue template 2023-08-13 16:57:27 +08:00
世界
3983f88a55
Update documentation 2023-08-13 16:57:27 +08:00
世界
4925553aa3
Fix ci build 2023-08-12 19:39:06 +08:00
世界
422bee9078
Add TCP MultiPath support 2023-08-12 19:39:05 +08:00
世界
ea2cf051ff
Pause recurring tasks when no network 2023-08-12 19:39:04 +08:00
世界
5b00f66533
documentation: Add TUIC 2023-08-12 19:39:04 +08:00
世界
825952e0bb
Add TUIC protocol 2023-08-12 19:39:03 +08:00
世界
43107187cd
Update quic-go 2023-08-12 19:39:03 +08:00
世界
4dce25c6cc
Update dependencies 2023-08-12 19:38:56 +08:00
世界
408903faab
Fix missing HandshakeConn interface 2023-08-12 19:00:35 +08:00
世界
58413bc010
platform: Enable Clash API support by default 2023-08-12 19:00:35 +08:00
世界
298d3cc054
Save fakeip metadata immediately 2023-08-11 19:22:21 +08:00
世界
6ec8ff79b5
Fix network monitor 2023-08-07 22:37:17 +08:00
113 changed files with 5529 additions and 844 deletions

View File

@ -1,70 +1,77 @@
name: Bug Report
description: "Create a report to help us improve."
name: Bug report
description: "Report sing-box bug"
body:
- type: checkboxes
id: terms
- type: dropdown
attributes:
label: Welcome
label: Operating system
description: Operating system type
options:
- label: Yes, I'm using the latest major release. Only such installations are supported.
required: true
- label: Yes, I'm using the latest Golang release. Only such installations are supported.
required: true
- label: Yes, I've searched similar issues on GitHub and didn't find any.
required: true
- label: Yes, I've included all information below (version, **FULL** config, **FULL** log, etc).
required: true
- type: textarea
id: problem
attributes:
label: Description of the problem
placeholder: Your problem description
- iOS
- macOS
- Apple tvOS
- Android
- Windows
- Linux
- Others
validations:
required: true
- type: textarea
id: version
- type: input
attributes:
label: Version of sing-box
label: System version
description: Please provide the operating system version
validations:
required: true
- type: dropdown
attributes:
label: Installation type
description: Please provide the sing-box installation type
options:
- Original sing-box Command Line
- sing-box for iOS Graphical Client
- sing-box for macOS Graphical Client
- sing-box for Apple tvOS Graphical Client
- sing-box for Android Graphical Client
- Third-party graphical clients that advertise themselves as using sing-box (Windows)
- Third-party graphical clients that advertise themselves as using sing-box (Android)
- Others
validations:
required: true
- type: input
attributes:
description: Graphical client version
label: If you are using a graphical client, please provide the version of the client.
- type: textarea
attributes:
label: Version
description: If you are using the original command line program, please provide the output of the `sing-box version` command.
value: |-
<details>
```console
$ sing-box version
# Paste output here
# Replace this line with the output
```
</details>
- type: textarea
attributes:
label: Description
description: Please provide a detailed description of the error.
validations:
required: true
- type: textarea
id: config
attributes:
label: Server and client configuration file
label: Reproduction
description: Please provide the steps to reproduce the error, including the configuration files and procedures that can locally (not dependent on the remote server) reproduce the error using the original command line program of sing-box.
validations:
required: true
- type: textarea
attributes:
label: Logs
description: |-
If you encounter a crash with the graphical client, please provide crash logs.
For Apple platform clients, please check `Settings - View Service Log` for crash logs.
For the Android client, please check the `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` file for crash logs.
value: |-
<details>
```console
# paste json here
# Replace this line with logs
```
</details>
validations:
required: true
- type: textarea
id: log
attributes:
label: Server and client log file
value: |-
<details>
```console
# paste log here
```
</details>
validations:
required: true
</details>

View File

@ -0,0 +1,77 @@
name: 错误反馈
description: "提交 sing-box 漏洞"
body:
- type: dropdown
attributes:
label: 操作系统
description: 请提供操作系统类型
options:
- iOS
- macOS
- Apple tvOS
- Android
- Windows
- Linux
- 其他
validations:
required: true
- type: input
attributes:
label: 系统版本
description: 请提供操作系统版本
validations:
required: true
- type: dropdown
attributes:
label: 安装类型
description: 请提供该 sing-box 安装类型
options:
- sing-box 原始命令行程序
- sing-box for iOS 图形客户端程序
- sing-box for macOS 图形客户端程序
- sing-box for Apple tvOS 图形客户端程序
- sing-box for Android 图形客户端程序
- 宣传使用 sing-box 的第三方图形客户端程序 (Windows)
- 宣传使用 sing-box 的第三方图形客户端程序 (Android)
- 其他
validations:
required: true
- type: input
attributes:
description: 图形客户端版本
label: 如果您使用图形客户端程序,请提供该程序版本。
- type: textarea
attributes:
label: 版本
description: 如果您使用原始命令行程序,请提供 `sing-box version` 命令的输出。
value: |-
<details>
```console
# 使用输出内容覆盖此行
```
</details>
- type: textarea
attributes:
label: 描述
description: 请提供错误的详细描述。
validations:
required: true
- type: textarea
attributes:
label: 重现方式
description: 请提供重现错误的步骤,必须包括可以在本地(不依赖与远程服务器)使用 sing-box 原始命令行程序重现错误的配置文件与流程。
validations:
required: true
- type: textarea
attributes:
label: 日志
description: |-
如果您遭遇图形界面应用程序崩溃,请提供崩溃日志。
对于 Apple 平台图形客户端程序,请检查 `Settings - View Service Log` 以导出崩溃日志。
对于 Android 图形客户端程序,请检查 `/sdcard/Android/data/io.nekohasekai.sfa/files/stderr.log` 文件以导出崩溃日志。
value: |-
<details>
```console
# 使用日志内容覆盖此行
```
</details>

View File

@ -62,7 +62,27 @@ jobs:
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make
run: make ci_build_go118
build_go120:
name: Debug build (Go 1.20)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: 1.20.7
- name: Cache go module
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: make ci_build
cross:
strategy:
matrix:

View File

@ -1,4 +1,4 @@
FROM golang:1.20-alpine AS builder
FROM golang:1.21-alpine AS builder
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box

View File

@ -1,20 +1,30 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_GO118 = with_gvisor,with_dhcp,with_wireguard,with_utls,with_reality_server,with_clash_api
TAGS_GO120 ?= with_quic
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_reality_server,with_shadowsocksr
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run ./cmd/internal/read_tag)
PARAMS = -v -trimpath -tags "$(TAGS)" -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_GO118),$(TAGS_GO120)"
MAIN = ./cmd/sing-box
PREFIX ?= $(shell go env GOPATH)
.PHONY: test release
build:
go build $(MAIN_PARAMS) $(MAIN)
ci_build_go118:
go build $(PARAMS) $(MAIN)
go build $(PARAMS) -tags "$(TAGS_GO118)" $(MAIN)
ci_build:
go build $(PARAMS) $(MAIN)
go build $(MAIN_PARAMS) $(MAIN)
install:
go build -o $(PREFIX)/bin/$(NAME) $(PARAMS) $(MAIN)

View File

@ -23,6 +23,8 @@ type ClashServer interface {
type ClashCacheFile interface {
LoadSelected(group string) string
StoreSelected(group string, selected string) error
LoadGroupExpand(group string) (isExpand bool, loaded bool)
StoreGroupExpand(group string, expand bool) error
FakeIPStorage
}

View File

@ -18,6 +18,7 @@ type FakeIPStore interface {
type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPSaveMetadataAsync(metadata *FakeIPMetadata)
FakeIPStore(address netip.Addr, domain string) error
FakeIPStoreAsync(address netip.Addr, domain string, logger logger.Logger)
FakeIPLoad(address netip.Addr) (string, bool)

View File

@ -85,5 +85,5 @@ type DNSRule interface {
}
type InterfaceUpdateListener interface {
InterfaceUpdated() error
InterfaceUpdated()
}

8
box.go
View File

@ -19,6 +19,7 @@ import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.Service = (*Box)(nil)
@ -46,12 +47,13 @@ func New(options Options) (*Box, error) {
if ctx == nil {
ctx = context.Background()
}
ctx = pause.ContextWithDefaultManager(ctx)
createdAt := time.Now()
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug))
var needClashAPI bool
var needV2RayAPI bool
if experimentalOptions.ClashAPI != nil && experimentalOptions.ClashAPI.ExternalController != "" {
if experimentalOptions.ClashAPI != nil || options.PlatformInterface != nil {
needClashAPI = true
}
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
@ -143,7 +145,7 @@ func New(options Options) (*Box, error) {
preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needClashAPI {
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(options.Experimental.ClashAPI))
clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), common.PtrValueOrDefault(experimentalOptions.ClashAPI))
if err != nil {
return nil, E.Cause(err, "create clash api server")
}
@ -151,7 +153,7 @@ func New(options Options) (*Box, error) {
preServices["clash api"] = clashServer
}
if needV2RayAPI {
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
if err != nil {
return nil, E.Cause(err, "create v2ray api server")
}

View File

@ -5,8 +5,10 @@ package badtls
import (
"crypto/tls"
"os"
aTLS "github.com/sagernet/sing/common/tls"
)
func Create(conn *tls.Conn) (TLSConn, error) {
func Create(conn *tls.Conn) (aTLS.Conn, error) {
return nil, os.ErrInvalid
}

View File

@ -26,7 +26,7 @@ type DefaultDialer struct {
udpAddr6 string
}
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) {
var dialer net.Dialer
var listener net.ListenConfig
if options.BindInterface != "" {
@ -93,6 +93,12 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
}
if options.TCPMultiPath {
if !multipathTCPAvailable {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&dialer4)
}
return &DefaultDialer{
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
@ -101,7 +107,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
listener,
udpAddr4,
udpAddr6,
}
}, nil
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {

View File

@ -0,0 +1,11 @@
//go:build go1.21
package dialer
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(dialer *net.Dialer) {
dialer.SetMultipathTCP(true)
}

View File

@ -0,0 +1,12 @@
//go:build !go1.21
package dialer
import (
"net"
)
const multipathTCPAvailable = false
func setMultiPathTCP(dialer *net.Dialer) {
}

View File

@ -6,13 +6,24 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common"
N "github.com/sagernet/sing/common/network"
)
func New(router adapter.Router, options option.DialerOptions) N.Dialer {
var dialer N.Dialer
func MustNew(router adapter.Router, options option.DialerOptions) N.Dialer {
return common.Must1(New(router, options))
}
func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) {
var (
dialer N.Dialer
err error
)
if options.Detour == "" {
dialer = NewDefault(router, options)
dialer, err = NewDefault(router, options)
if err != nil {
return nil, err
}
} else {
dialer = NewDetour(router, options.Detour)
}
@ -20,5 +31,5 @@ func New(router adapter.Router, options option.DialerOptions) N.Dialer {
if domainStrategy != dns.DomainStrategyAsIS || options.Detour == "" {
dialer = NewResolveDialer(router, dialer, domainStrategy, time.Duration(options.FallbackDelay))
}
return dialer
return dialer, nil
}

View File

@ -20,10 +20,10 @@ type systemProxy struct {
isMixed bool
}
func (p *systemProxy) update(event int) error {
func (p *systemProxy) update(event int) {
newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified())
if p.interfaceName == newInterfaceName {
return nil
return
}
if p.interfaceName != "" {
_ = p.unset()
@ -31,7 +31,7 @@ func (p *systemProxy) update(event int) error {
p.interfaceName = newInterfaceName
interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName)
if err != nil {
return err
return
}
if p.isMixed {
err = shell.Exec("networksetup", "-setsocksfirewallproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
@ -40,9 +40,9 @@ func (p *systemProxy) update(event int) error {
err = shell.Exec("networksetup", "-setwebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
if err == nil {
err = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
_ = shell.Exec("networksetup", "-setsecurewebproxy", interfaceDisplayName, "127.0.0.1", F.ToString(p.port)).Attach().Run()
}
return err
return
}
func (p *systemProxy) unset() error {
@ -88,10 +88,7 @@ func SetSystemProxy(router adapter.Router, port uint16, isMixed bool) (func() er
port: port,
isMixed: isMixed,
}
err := proxy.update(tun.EventInterfaceUpdate)
if err != nil {
return nil, err
}
proxy.update(tun.EventInterfaceUpdate)
proxy.element = interfaceMonitor.RegisterCallback(proxy.update)
return func() error {
interfaceMonitor.UnregisterCallback(proxy.element)

View File

@ -1,43 +0,0 @@
package sleep
import (
"sync"
)
type Manager struct {
access sync.Mutex
done chan struct{}
}
func NewManager() *Manager {
closedChan := make(chan struct{})
close(closedChan)
return &Manager{
done: closedChan,
}
}
func (m *Manager) Sleep() {
m.access.Lock()
defer m.access.Unlock()
select {
case <-m.done:
default:
return
}
m.done = make(chan struct{})
}
func (m *Manager) Wake() {
m.access.Lock()
defer m.access.Unlock()
select {
case <-m.done:
default:
close(m.done)
}
}
func (m *Manager) Active() <-chan struct{} {
return m.done
}

View File

@ -101,7 +101,10 @@ func NewRealityServer(ctx context.Context, router adapter.Router, logger log.Log
tlsConfig.ShortIds[shortID] = true
}
handshakeDialer := dialer.New(router, options.Reality.Handshake.DialerOptions)
handshakeDialer, err := dialer.New(router, options.Reality.Handshake.DialerOptions)
if err != nil {
return nil, err
}
tlsConfig.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}

View File

@ -164,8 +164,8 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
var acmeService adapter.Service
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
//nolint:staticcheck
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
if err != nil {
return nil, err
}

View File

@ -21,6 +21,7 @@ const (
TypeShadowTLS = "shadowtls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
)
const (
@ -62,6 +63,8 @@ func ProxyDisplayName(proxyType string) string {
return "ShadowsocksR"
case TypeVLESS:
return "VLESS"
case TypeTUIC:
return "TUIC"
case TypeSelector:
return "Selector"
case TypeURLTest:

View File

@ -0,0 +1,82 @@
### Structure
```json
{
"type": "tuic",
"tag": "tuic-in",
... // Listen Fields
"users": [
{
"name": "sekai",
"uuid": "059032A9-7D40-4A96-9BB1-36823D848068",
"password": "hello"
}
],
"congestion_control": "cubic",
"auth_timeout": "3s",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"tls": {}
}
```
!!! warning ""
QUIC, which is required by TUIC is not included by default, see [Installation](/#installation).
### Listen Fields
See [Listen Fields](/configuration/shared/listen) for details.
### Fields
#### users
TUIC users
#### users.uuid
==Required==
TUIC user uuid
#### users.password
TUIC user password
#### congestion_control
QUIC congestion control algorithm
One of: `cubic`, `new_reno`, `bbr`
`cubic` is used by default.
#### auth_timeout
How long the server should wait for the client to send the authentication command
`3s` is used by default.
#### zero_rtt_handshake
Enable 0-RTT QUIC connection handshake on the client side
This is not impacting much on the performance, as the protocol is fully multiplexed
!!! warning ""
Disabling this is highly recommended, as it is vulnerable to replay attacks.
See [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
#### heartbeat
Interval for sending heartbeat packets for keeping the connection alive
`10s` is used by default.
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#inbound).

View File

@ -0,0 +1,82 @@
### 结构
```json
{
"type": "tuic",
"tag": "tuic-in",
... // 监听字段
"users": [
{
"name": "sekai",
"uuid": "059032A9-7D40-4A96-9BB1-36823D848068",
"password": "hello"
}
],
"congestion_control": "cubic",
"auth_timeout": "3s",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"tls": {}
}
```
!!! warning ""
默认安装不包含被 TUI 依赖的 QUIC参阅 [安装](/zh/#_2)。
### 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/)。
### 字段
#### users
TUIC 用户
#### users.uuid
==必填==
TUIC 用户 UUID
#### users.password
TUIC 用户密码
#### congestion_control
QUIC 流量控制算法
可选值: `cubic`, `new_reno`, `bbr`
默认使用 `cubic`
#### auth_timeout
服务器等待客户端发送认证命令的时间
默认使用 `3s`
#### zero_rtt_handshake
在客户端启用 0-RTT QUIC 连接握手
这对性能影响不大,因为协议是完全复用的
!!! warning ""
强烈建议禁用此功能,因为它容易受到重放攻击。
请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
#### heartbeat
发送心跳包以保持连接存活的时间间隔
默认使用 `10s`
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。

View File

@ -142,11 +142,12 @@ UDP NAT expiration time in seconds, default is 300 (5 minutes).
TCP/IP stack.
| Stack | Description | Status |
|------------------|----------------------------------------------------------------------------------|-------------------|
| system (default) | Sometimes better performance | recommended |
| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended |
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
| Stack | Description | Status |
|--------|----------------------------------------------------------------------------------|-------------------|
| system | Sometimes better performance | recommended |
| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended |
| mixed | Mixed `system` TCP stack and `gVisor` UDP stack | recommended |
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
!!! warning ""

View File

@ -97,12 +97,6 @@ Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4)
Force enabled on for systems other than Linux and Windows (according to upstream).
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
#### network
Enabled network
@ -111,6 +105,12 @@ One of `tcp` `udp`.
Both is enabled by default.
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
See [Dial Fields](/configuration/shared/dial) for details.

View File

@ -97,10 +97,6 @@ base64 编码的认证密码。
强制为 Linux 和 Windows 以外的系统启用(根据上游)。
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### network
启用的网络协议。
@ -109,6 +105,13 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
默认所有。
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@ -0,0 +1,86 @@
### Structure
```json
{
"type": "tuic",
"tag": "tuic-out",
"server": "127.0.0.1",
"server_port": 1080,
"uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365",
"password": "hello",
"congestion_control": "cubic",
"udp_relay_mode": "native",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"network": "tcp",
"tls": {},
... // Dial Fields
}
```
!!! warning ""
QUIC, which is required by TUIC is not included by default, see [Installation](/#installation).
### Fields
#### server
==Required==
The server address.
#### server_port
==Required==
The server port.
#### uuid
==Required==
TUIC user uuid
#### password
TUIC user password
#### congestion_control
QUIC congestion control algorithm
One of: `cubic`, `new_reno`, `bbr`
`cubic` is used by default.
#### udp_relay_mode
UDP packet relay mode
| Mode | Description |
|:-------|:-------------------------------------------------------------------------|
| native | native UDP characteristics |
| quic | lossless UDP relay using QUIC streams, additional overhead is introduced |
`native` is used by default.
#### network
Enabled network
One of `tcp` `udp`.
Both is enabled by default.
#### tls
==Required==
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
### Dial Fields
See [Dial Fields](/configuration/shared/dial) for details.

View File

@ -0,0 +1,98 @@
### 结构
```json
{
"type": "tuic",
"tag": "tuic-out",
"server": "127.0.0.1",
"server_port": 1080,
"uuid": "2DD61D93-75D8-4DA4-AC0E-6AECE7EAC365",
"password": "hello",
"congestion_control": "cubic",
"udp_relay_mode": "native",
"zero_rtt_handshake": false,
"heartbeat": "10s",
"network": "tcp",
"tls": {},
... // 拨号字段
}
```
!!! warning ""
默认安装不包含被 TUI 依赖的 QUIC参阅 [安装](/zh/#_2)。
### 字段
#### server
==必填==
服务器地址。
#### server_port
==必填==
服务器端口。
#### uuid
==必填==
TUIC 用户 UUID
#### password
TUIC 用户密码
#### congestion_control
QUIC 流量控制算法
可选值: `cubic`, `new_reno`, `bbr`
默认使用 `cubic`
#### udp_relay_mode
UDP 包中继模式
| 模式 | 描述 |
|--------|------------------------------|
| native | 原生 UDP |
| quic | 使用 QUIC 流的无损 UDP 中继,引入了额外的开销 |
#### zero_rtt_handshake
在客户端启用 0-RTT QUIC 连接握手
这对性能影响不大,因为协议是完全复用的
!!! warning ""
强烈建议禁用此功能,因为它容易受到重放攻击。
请参阅 [Attack of the clones](https://blog.cloudflare.com/even-faster-connection-establishment-with-quic-0-rtt-resumption/#attack-of-the-clones)
#### heartbeat
发送心跳包以保持连接存活的时间间隔
#### network
启用的网络协议。
`tcp``udp`
默认所有。
#### tls
==必填==
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
### 拨号字段
参阅 [拨号字段](/zh/configuration/shared/dial/)。

View File

@ -24,7 +24,6 @@
|------------|-------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
| `rules` | 一组 [路由规则](./rule) |
#### final

View File

@ -1,205 +0,0 @@
### Structure
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Default Fields
!!! note ""
The default rule uses the following matching logic:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
Tags of [Inbound](/configuration/inbound).
#### ip_version
4 or 6.
Not limited if empty.
#### network
Match network protocol.
Available values:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
Match full domain.
#### domain_suffix
Match domain suffix.
#### domain_keyword
Match domain using keyword.
#### domain_regex
Match domain using regular expression.
#### geosite
Match geosite.
#### source_geoip
Match source geoip.
#### geoip
Match geoip.
#### source_ip_cidr
Match source ip cidr.
#### ip_cidr
Match ip cidr.
#### source_port
Match source port.
#### source_port_range
Match source port range.
#### port
Match port.
#### port_range
Match port range.
#### invert
Invert match result.
#### action
==Required==
| Action | Description |
|--------|--------------------------------------------------------------------|
| return | Stop IP routing and assemble the connection to the transport layer |
| block | Block the connection |
| direct | Directly forward the connection |
#### outbound
==Required if action is direct==
Tag of the target outbound.
Only outbound which supports IP connection can be used, see [Outbounds that support IP connection](/configuration/outbound/#outbounds-that-support-ip-connection).
### Logical Fields
#### type
`logical`
#### mode
==Required==
`and` or `or`
#### rules
==Required==
Included default rules.

View File

@ -1,204 +0,0 @@
### 结构
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
### Default Fields
!!! note ""
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
[入站](/zh/configuration/inbound) 标签。
#### ip_version
4 或 6。
默认不限制。
#### network
匹配网络协议。
可用值:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
匹配完整域名。
#### domain_suffix
匹配域名后缀。
#### domain_keyword
匹配域名关键字。
#### domain_regex
匹配域名正则表达式。
#### geosite
匹配 GeoSite。
#### source_geoip
匹配源 GeoIP。
#### geoip
匹配 GeoIP。
#### source_ip_cidr
匹配源 IP CIDR。
#### ip_cidr
匹配 IP CIDR。
#### source_port
匹配源端口。
#### source_port_range
匹配源端口范围。
#### port
匹配端口。
#### port_range
匹配端口范围。
#### invert
反选匹配结果。
#### action
==必填==
| Action | 描述 |
|--------|---------------------|
| return | 停止 IP 路由并将该连接组装到传输层 |
| block | 屏蔽该连接 |
| direct | 直接转发该连接 |
#### outbound
==action 为 direct 则必填==
目标出站的标签。
### 逻辑字段
#### type
`logical`
#### mode
==必填==
`and``or`
#### rules
==必填==
包括的默认规则。

View File

@ -10,6 +10,7 @@
"reuse_addr": false,
"connect_timeout": "5s",
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
"domain_strategy": "prefer_ipv6",
"fallback_delay": "300ms"
@ -18,9 +19,9 @@
### Fields
| Field | Available Context |
|----------------------------------------------------------------------------------------------------------------------|-------------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set |
| Field | Available Context |
|------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_multi_path` / `udp_fragment` /`connect_timeout` | `detour` not set |
#### detour
@ -54,6 +55,14 @@ Reuse listener address.
Enable TCP Fast Open.
#### tcp_multi_path
!!! warning ""
Go 1.21 required.
Enable TCP Multi Path.
#### udp_fragment
Enable UDP fragmentation.

View File

@ -10,6 +10,7 @@
"reuse_addr": false,
"connect_timeout": "5s",
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
"domain_strategy": "prefer_ipv6",
"fallback_delay": "300ms"
@ -18,9 +19,9 @@
### 字段
| 字段 | 可用上下文 |
|----------------------------------------------------------------------------------------------------------------------|--------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
| 字段 | 可用上下文 |
|------------------------------------------------------------------------------------------------------------------------------------------|--------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_mutli_path` / `udp_fragment` /`connect_timeout` | `detour` 未设置 |
#### detour
@ -57,6 +58,14 @@
启用 TCP Fast Open。
#### tcp_multi_path
!!! warning ""
需要 Go 1.21。
启用 TCP Multi Path。
#### udp_fragment
启用 UDP 分段。

View File

@ -5,6 +5,7 @@
"listen": "::",
"listen_port": 5353,
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
@ -24,6 +25,7 @@
| `listen` | Needs to listen on TCP or UDP. |
| `listen_port` | Needs to listen on TCP or UDP. |
| `tcp_fast_open` | Needs to listen on TCP. |
| `tcp_multi_path` | Needs to listen on TCP. |
| `udp_timeout` | Needs to assemble UDP connections, currently Tun and Shadowsocks. |
| `proxy_protocol` | Needs to listen on TCP. |
| `proxy_protocol_accept_no_header` | When `proxy_protocol` enabled |
@ -42,6 +44,14 @@ Listen port.
Enable TCP Fast Open.
#### tcp_multi_path
!!! warning ""
Go 1.21 required.
Enable TCP Multi Path.
#### udp_fragment
Enable UDP fragmentation.

View File

@ -5,6 +5,7 @@
"listen": "::",
"listen_port": 5353,
"tcp_fast_open": false,
"tcp_multi_path": false,
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
@ -23,6 +24,7 @@
| `listen` | 需要监听 TCP 或 UDP。 |
| `listen_port` | 需要监听 TCP 或 UDP。 |
| `tcp_fast_open` | 需要监听 TCP。 |
| `tcp_multi_path` | 需要监听 TCP。 |
| `udp_timeout` | 需要组装 UDP 连接, 当前为 Tun 和 Shadowsocks。 |
| `proxy_protocol` | 需要监听 TCP。 |
| `proxy_protocol_accept_no_header` | `proxy_protocol` 启用时 |
@ -43,6 +45,14 @@
启用 TCP Fast Open。
#### tcp_multi_path
!!! warning ""
需要 Go 1.21。
启用 TCP Multi Path。
#### udp_fragment
启用 UDP 分段。

View File

@ -8,5 +8,4 @@ Configuration examples for sing-box.
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)
* [FakeIP](./fakeip)

View File

@ -8,5 +8,4 @@ sing-box 的配置示例。
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)
* [FakeIP](./fakeip)

View File

@ -1,90 +0,0 @@
# WireGuard Direct
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"geoip": "cn",
"server": "direct"
}
],
"reverse_mapping": true
},
"inbounds": [
{
"type": "tun",
"tag": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"sniff": true,
"stack": "system"
}
],
"outbounds": [
{
"type": "wireguard",
"tag": "wg",
"server": "127.0.0.1",
"server_port": 2345,
"local_address": [
"172.19.0.1/128"
],
"private_key": "KLTnpPY03pig/WC3zR8U7VWmpANHPFh2/4pwICGJ5Fk=",
"peer_public_key": "uvNabcamf6Rs0vzmcw99jsjTJbxo6eWGOykSY66zsUk="
},
{
"type": "dns",
"tag": "dns"
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"ip_rules": [
{
"port": 53,
"action": "return"
},
{
"geoip": "cn",
"geosite": "cn",
"action": "return"
},
{
"action": "direct",
"outbound": "wg"
}
],
"rules": [
{
"protocol": "dns",
"outbound": "dns"
},
{
"geoip": "cn",
"geosite": "cn",
"outbound": "direct"
}
],
"auto_detect_interface": true
}
}
```

View File

@ -9,7 +9,7 @@ Experimental Android client for sing-box.
#### Download
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
* [Github Releases](https://SagerNet/sing-box/releases)
* [Github Releases](https://github.com/SagerNet/sing-box/releases)
#### Note

View File

@ -9,7 +9,7 @@
#### 下载
* [AppCenter](https://install.appcenter.ms/users/nekohasekai/apps/sfa/distribution_groups/publictest)
* [Github Releases](https://SagerNet/sing-box/releases)
* [Github Releases](https://github.com/SagerNet/sing-box/releases)
#### 注意事项

View File

@ -1,6 +1,17 @@
# Install from source
sing-box requires Golang **1.18.5** or a higher version.
## Requirements
Before sing-box 1.4.0:
* Go 1.18.5 - 1.20.x
Since sing-box 1.4.0:
* Go 1.18.5 - ~
* Go 1.20.0 - ~ if `with_quic` tag enabled
## Installation
```bash
go install -v github.com/sagernet/sing-box/cmd/sing-box@latest
@ -9,7 +20,7 @@ go install -v github.com/sagernet/sing-box/cmd/sing-box@latest
Install with options:
```bash
go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@latest
go install -v -tags with_quic,with_wireguard github.com/sagernet/sing-box/cmd/sing-box@latest
```
| Build Tag | Description |

View File

@ -12,17 +12,21 @@ import (
"go.etcd.io/bbolt"
)
var bucketSelected = []byte("selected")
var (
bucketSelected = []byte("selected")
bucketExpand = []byte("group_expand")
)
var _ adapter.ClashCacheFile = (*CacheFile)(nil)
type CacheFile struct {
DB *bbolt.DB
cacheID []byte
saveAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
DB *bbolt.DB
cacheID []byte
saveAccess sync.RWMutex
saveDomain map[netip.Addr]string
saveAddress4 map[string]netip.Addr
saveAddress6 map[string]netip.Addr
saveMetadataTimer *time.Timer
}
func Open(path string, cacheID string) (*CacheFile, error) {
@ -48,21 +52,15 @@ func Open(path string, cacheID string) (*CacheFile, error) {
if name[0] == 0 {
return b.ForEachBucket(func(k []byte) error {
bucketName := string(k)
if !(bucketName == string(bucketSelected)) {
delErr := b.DeleteBucket(name)
if delErr != nil {
return delErr
}
if !(bucketName == string(bucketSelected) || bucketName == string(bucketExpand)) {
_ = b.DeleteBucket(name)
}
return nil
})
} else {
bucketName := string(name)
if !(bucketName == string(bucketSelected) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
delErr := tx.DeleteBucket(name)
if delErr != nil {
return delErr
}
if !(bucketName == string(bucketSelected) || bucketName == string(bucketExpand) || strings.HasPrefix(bucketName, fakeipBucketPrefix)) {
_ = tx.DeleteBucket(name)
}
}
return nil
@ -128,6 +126,36 @@ func (c *CacheFile) StoreSelected(group, selected string) error {
})
}
func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
c.DB.View(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketExpand)
if bucket == nil {
return nil
}
expandBytes := bucket.Get([]byte(group))
if len(expandBytes) == 1 {
isExpand = expandBytes[0] == 1
loaded = true
}
return nil
})
return
}
func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketExpand)
if err != nil {
return err
}
if isExpand {
return bucket.Put([]byte(group), []byte{1})
} else {
return bucket.Put([]byte(group), []byte{0})
}
})
}
func (c *CacheFile) Close() error {
return c.DB.Close()
}

View File

@ -3,6 +3,7 @@ package cachefile
import (
"net/netip"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
@ -57,6 +58,15 @@ func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
})
}
func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
if timer := c.saveMetadataTimer; timer != nil {
timer.Stop()
}
c.saveMetadataTimer = time.AfterFunc(10*time.Second, func() {
_ = c.FakeIPSaveMetadata(metadata)
})
}
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)

View File

@ -52,6 +52,7 @@ type Server struct {
cacheID string
cacheFile adapter.ClashCacheFile
externalController bool
externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
@ -71,6 +72,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
trafficManager: trafficManager,
mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected,
externalController: options.ExternalController != "",
storeFakeIP: options.StoreFakeIP,
externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
@ -82,7 +84,7 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
if server.mode == "" {
server.mode = "rule"
}
if options.StoreSelected || options.StoreFakeIP {
if options.StoreSelected || options.StoreFakeIP || options.ExternalController == "" {
cachePath := os.ExpandEnv(options.CacheFile)
if cachePath == "" {
cachePath = "cache.db"
@ -146,18 +148,20 @@ func (s *Server) PreStart() error {
}
func (s *Server) Start() error {
s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
}
s.logger.Info("restful api listening at ", listener.Addr())
go func() {
err = s.httpServer.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("external controller serve error: ", err)
if s.externalController {
s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
}
}()
s.logger.Info("restful api listening at ", listener.Addr())
go func() {
err = s.httpServer.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("external controller serve error: ", err)
}
}()
}
return nil
}

View File

@ -8,4 +8,5 @@ const (
CommandGroup
CommandSelectOutbound
CommandURLTest
CommandGroupExpand
)

View File

@ -9,6 +9,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/service"
)
@ -18,6 +19,7 @@ type OutboundGroup struct {
Type string
Selectable bool
Selected string
isExpand int8
items []*OutboundGroupItem
}
@ -25,6 +27,19 @@ func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.items)
}
func (g *OutboundGroup) IsExpand() bool {
switch g.isExpand {
case -1:
return g.Selectable
case 0:
return false
case 1:
return true
default:
panic("unexpected expand value")
}
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
@ -114,6 +129,11 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &group.isExpand)
if err != nil {
return nil, err
}
var itemLength uint16
err = binary.Read(reader, binary.BigEndian, &itemLength)
if err != nil {
@ -152,6 +172,10 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
func writeGroups(writer io.Writer, boxService *BoxService) error {
historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
var cacheFile adapter.ClashCacheFile
if clashServer := boxService.instance.Router().ClashServer(); clashServer != nil {
cacheFile = clashServer.CacheFile()
}
outbounds := boxService.instance.Router().Outbounds()
var iGroups []adapter.OutboundGroup
@ -167,6 +191,15 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
group.Type = iGroup.Type()
_, group.Selectable = iGroup.(*outbound.Selector)
group.Selected = iGroup.Now()
if cacheFile != nil {
if isExpand, loaded := cacheFile.LoadGroupExpand(group.Tag); !loaded {
group.isExpand = -1
} else if isExpand {
group.isExpand = 1
} else {
group.isExpand = 0
}
}
for _, itemTag := range iGroup.All() {
itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag)
@ -207,6 +240,10 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, group.isExpand)
if err != nil {
return err
}
err = binary.Write(writer, binary.BigEndian, uint16(len(group.items)))
if err != nil {
return err
@ -232,3 +269,50 @@ func writeGroups(writer io.Writer, boxService *BoxService) error {
}
return nil
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandGroupExpand))
if err != nil {
return err
}
err = rw.WriteVString(conn, groupTag)
if err != nil {
return err
}
err = binary.Write(conn, binary.BigEndian, isExpand)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
defer conn.Close()
groupTag, err := rw.ReadVString(conn)
if err != nil {
return err
}
var isExpand bool
err = binary.Read(conn, binary.BigEndian, &isExpand)
if err != nil {
return err
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
if clashServer := service.instance.Router().ClashServer(); clashServer != nil {
if cacheFile := clashServer.CacheFile(); cacheFile != nil {
err = cacheFile.StoreGroupExpand(groupTag, isExpand)
if err != nil {
return writeError(conn, err)
}
}
}
return writeError(conn, nil)
}

View File

@ -154,6 +154,8 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
return s.handleSelectOutbound(conn)
case CommandURLTest:
return s.handleURLTest(conn)
case CommandGroupExpand:
return s.handleSetGroupExpand(conn)
default:
return E.New("unknown command: ", command)
}

View File

@ -1,7 +1,6 @@
package libbox
import (
"context"
"net"
"net/netip"
"sync"
@ -9,6 +8,7 @@ import (
"github.com/sagernet/sing-tun"
"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/common/x/list"
)
@ -20,13 +20,13 @@ var (
type platformDefaultInterfaceMonitor struct {
*platformInterfaceWrapper
errorHandler E.Handler
networkAddresses []networkAddress
defaultInterfaceName string
defaultInterfaceIndex int
element *list.Element[tun.NetworkUpdateCallback]
access sync.Mutex
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
logger logger.Logger
}
type networkAddress struct {
@ -96,7 +96,7 @@ func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName s
err = m.router.UpdateInterfaces()
}
if err != nil {
m.errorHandler.NewError(context.Background(), E.Cause(err, "update interfaces"))
m.logger.Error(E.Cause(err, "update interfaces"))
}
interfaceIndex := int(interfaceIndex32)
if interfaceName == "" {
@ -115,10 +115,10 @@ func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName s
}
}
if interfaceName == "" {
m.errorHandler.NewError(context.Background(), E.New("invalid interface name for ", interfaceIndex))
m.logger.Error(E.New("invalid interface name for ", interfaceIndex))
return
} else if interfaceIndex == -1 {
m.errorHandler.NewError(context.Background(), E.New("invalid interface index for ", interfaceName))
m.logger.Error(E.New("invalid interface index for ", interfaceName))
return
}
if m.defaultInterfaceName == interfaceName && m.defaultInterfaceIndex == interfaceIndex {
@ -130,10 +130,7 @@ func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName s
callbacks := m.callbacks.Array()
m.access.Unlock()
for _, callback := range callbacks {
err = callback(tun.EventInterfaceUpdate)
if err != nil {
m.errorHandler.NewError(context.Background(), err)
}
callback(tun.EventInterfaceUpdate)
}
}

View File

@ -10,7 +10,7 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
)
type Interface interface {
@ -19,7 +19,7 @@ type Interface interface {
AutoDetectInterfaceControl() control.Func
OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(errorHandler E.Handler) tun.DefaultInterfaceMonitor
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
UsePlatformInterfaceGetter() bool
Interfaces() ([]NetworkInterface, error)
UnderNetworkExtension() bool

View File

@ -3,12 +3,12 @@ package libbox
import (
"context"
"net/netip"
runtimeDebug "runtime/debug"
"syscall"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/common/sleep"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
"github.com/sagernet/sing-box/experimental/libbox/platform"
@ -17,16 +17,18 @@ import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/sing/service/pause"
)
type BoxService struct {
ctx context.Context
cancel context.CancelFunc
instance *box.Box
sleepManager *sleep.Manager
pauseManager pause.Manager
}
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
@ -34,11 +36,12 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
if err != nil {
return nil, err
}
runtimeDebug.FreeOSMemory()
ctx, cancel := context.WithCancel(context.Background())
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
sleepManager := sleep.NewManager()
ctx = service.ContextWithPtr(ctx, sleepManager)
sleepManager := pause.NewDefaultManager(ctx)
ctx = pause.ContextWithManager(ctx, sleepManager)
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
@ -48,11 +51,12 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
cancel()
return nil, E.Cause(err, "create service")
}
runtimeDebug.FreeOSMemory()
return &BoxService{
ctx: ctx,
cancel: cancel,
instance: instance,
sleepManager: sleepManager,
pauseManager: sleepManager,
}, nil
}
@ -66,12 +70,12 @@ func (s *BoxService) Close() error {
}
func (s *BoxService) Sleep() {
s.sleepManager.Sleep()
s.pauseManager.DevicePause()
_ = s.instance.Router().ResetNetwork()
}
func (s *BoxService) Wake() {
s.sleepManager.Wake()
s.pauseManager.DeviceWake()
}
var _ platform.Interface = (*platformInterfaceWrapper)(nil)
@ -158,11 +162,11 @@ func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
return w.iif.UsePlatformDefaultInterfaceMonitor()
}
func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(errorHandler E.Handler) tun.DefaultInterfaceMonitor {
func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
return &platformDefaultInterfaceMonitor{
platformInterfaceWrapper: w,
errorHandler: errorHandler,
defaultInterfaceIndex: -1,
logger: logger,
}
}

15
go.mod
View File

@ -23,21 +23,21 @@ require (
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111
github.com/sagernet/quic-go v0.0.0-20230811130919-d6f54a117913
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21
github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c
github.com/sagernet/sing-shadowsocks v0.2.4
github.com/sagernet/sing-shadowsocks2 v0.1.3
github.com/sagernet/sing-shadowtls v0.1.4
github.com/sagernet/sing-tun v0.1.11
github.com/sagernet/sing-tun v0.1.12-0.20230812113806-10d98f26797a
github.com/sagernet/sing-vmess v0.1.7
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37
github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2
github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
go.etcd.io/bbolt v1.3.7
@ -62,6 +62,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@ -76,9 +77,7 @@ require (
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect

48
go.sum
View File

@ -41,6 +41,7 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -93,12 +94,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
@ -110,26 +107,26 @@ github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTS
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111 h1:BjZBIgwzlIbRSijdGQYZf0CaqHY1ZEIOUqVEKEICU0U=
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111/go.mod h1:5rilP6WxqIl/4ypZbMjr+MK+STxuCEvO5yVtEyYNZ6g=
github.com/sagernet/quic-go v0.0.0-20230811130919-d6f54a117913 h1:4dyzZWAEo9BNQN7yJsVSiN/Pm1hmUfkGJdEyWMkUnVE=
github.com/sagernet/quic-go v0.0.0-20230811130919-d6f54a117913/go.mod h1:w+nln6f/ZtyPpGbFxmgd5iYFVMmgS+gpD5hu5GAqC1I=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee h1:5MATgtWMh2TCAVMtQnC3UcVMympANU7zXEekctD29PY=
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a h1:b89t6Mjgk4rJ5lrNMnCzy1/J116XkhgdB3YNd9FHyF4=
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 h1:1DAKccGNqTYJ8nsBR765FS0LVBVXfuFlFAHqKsGN3EI=
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659/go.mod h1:W7GHTZFS8RkoLI3bA2LFY27/0E+uoQESWtMFLepO/JA=
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21 h1:IQ7oBBKz+lwIqwI9IMStlQ9YSUu3eKJmNTip0aLbvOI=
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c h1:35/FowAvt3Z62mck0TXzVc4jS5R5CWq62qcV2P1cp0I=
github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
github.com/sagernet/sing-shadowsocks v0.2.4/go.mod h1:80fNKP0wnqlu85GZXV1H1vDPC/2t+dQbFggOw4XuFUM=
github.com/sagernet/sing-shadowsocks2 v0.1.3 h1:WXoLvCFi5JTFBRYorf1YePGYIQyJ/zbsBM6Fwbl5kGA=
github.com/sagernet/sing-shadowsocks2 v0.1.3/go.mod h1:DOhJc/cLeqRv0wuePrQso+iUmDxOnWF4eT/oMcRzYFw=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/sing-tun v0.1.11 h1:wUfRQZ4eHk8suHkGKEFxjV5uXl3tfZhPm/v14/4lHvk=
github.com/sagernet/sing-tun v0.1.11/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230812113806-10d98f26797a h1:YZ20/ohB4wDQlOd2SMaL+qnAoWyM2yuXIUOVjUqj87U=
github.com/sagernet/sing-tun v0.1.12-0.20230812113806-10d98f26797a/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-vmess v0.1.7 h1:TM8FFLsXmlXH9XT8/oDgc6PC5BOzrg6OzyEe01is2r4=
github.com/sagernet/sing-vmess v0.1.7/go.mod h1:1qkC1L1T2sxnS/NuO6HU72S8TkltV+EXoKGR29m/Yss=
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
@ -140,8 +137,8 @@ github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfI
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+VpWamge6o56j2RWk4omF6uLKKxMmcWvs=
github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY=
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77 h1:g6QtRWQ2dKX7EQP++1JLNtw4C2TNxd4/ov8YUpOPOSo=
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77/go.mod h1:pJDdXzZIwJ+2vmnT0TKzmf8meeum+e2mTDSehw79eE0=
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho=
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
@ -158,6 +155,7 @@ github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gV
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@ -173,25 +171,37 @@ go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c=
go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28 h1:zLxFnORHDFTSkJPawMU7LzsuGQJ4MUFS653jJHpORow=
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -200,6 +210,7 @@ golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
@ -207,9 +218,14 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=

View File

@ -44,6 +44,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
case C.TypeVLESS:
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
case C.TypeTUIC:
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions)
default:
return nil, E.New("unknown inbound type: ", options.Type)
}

View File

@ -153,6 +153,17 @@ func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.Inboun
return metadata
}
func (a *myInboundAdapter) createPacketMetadata(conn N.PacketConn, metadata adapter.InboundContext) adapter.InboundContext {
metadata.Inbound = a.tag
metadata.InboundType = a.protocol
metadata.InboundDetour = a.listenOptions.Detour
metadata.InboundOptions = a.listenOptions.InboundOptions
if !metadata.Destination.IsValid() {
metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap()
}
return metadata
}
func (a *myInboundAdapter) newError(err error) {
a.logger.Error(err)
}

View File

@ -18,7 +18,14 @@ func (a *myInboundAdapter) ListenTCP() (net.Listener, error) {
bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort)
var tcpListener net.Listener
if !a.listenOptions.TCPFastOpen {
tcpListener, err = net.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
var listenConfig net.ListenConfig
if a.listenOptions.TCPMultiPath {
if !multipathTCPAvailable {
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
}
setMultiPathTCP(&listenConfig)
}
tcpListener, err = listenConfig.Listen(a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String())
} else {
tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.TCPAddr())
}

View File

@ -0,0 +1,11 @@
//go:build go1.21
package inbound
import "net"
const multipathTCPAvailable = true
func setMultiPathTCP(listenConfig *net.ListenConfig) {
listenConfig.SetMultipathTCP(true)
}

View File

@ -0,0 +1,10 @@
//go:build !go1.21
package inbound
import "net"
const multipathTCPAvailable = false
func setMultiPathTCP(listenConfig *net.ListenConfig) {
}

View File

@ -244,7 +244,7 @@ func (h *Hysteria) accept(ctx context.Context, conn quic.Connection) error {
func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
for {
packet, err := conn.ReceiveMessage()
packet, err := conn.ReceiveMessage(h.ctx)
if err != nil {
return
}

View File

@ -40,12 +40,20 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
if options.Version > 1 {
handshakeForServerName = make(map[string]shadowtls.HandshakeConfig)
for serverName, serverOptions := range options.HandshakeForServerName {
handshakeDialer, err := dialer.New(router, serverOptions.DialerOptions)
if err != nil {
return nil, err
}
handshakeForServerName[serverName] = shadowtls.HandshakeConfig{
Server: serverOptions.ServerOptions.Build(),
Dialer: dialer.New(router, serverOptions.DialerOptions),
Dialer: handshakeDialer,
}
}
}
handshakeDialer, err := dialer.New(router, options.Handshake.DialerOptions)
if err != nil {
return nil, err
}
service, err := shadowtls.NewService(shadowtls.ServiceConfig{
Version: options.Version,
Password: options.Password,
@ -54,7 +62,7 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
}),
Handshake: shadowtls.HandshakeConfig{
Server: options.Handshake.ServerOptions.Build(),
Dialer: dialer.New(router, options.Handshake.DialerOptions),
Dialer: handshakeDialer,
},
HandshakeForServerName: handshakeForServerName,
StrictMode: options.StrictMode,

114
inbound/tuic.go Normal file
View File

@ -0,0 +1,114 @@
//go:build with_quic
package inbound
import (
"context"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"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-box/transport/tuic"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
)
var _ adapter.Inbound = (*TUIC)(nil)
type TUIC struct {
myInboundAdapter
server *tuic.Server
tlsConfig tls.ServerConfig
}
func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (*TUIC, error) {
options.UDPFragmentDefault = true
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}
tlsConfig, err := tls.NewServer(ctx, router, logger, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
rawConfig, err := tlsConfig.Config()
if err != nil {
return nil, err
}
var users []tuic.User
for index, user := range options.Users {
if user.UUID == "" {
return nil, E.New("missing uuid for user ", index)
}
userUUID, err := uuid.FromString(user.UUID)
if err != nil {
return nil, E.Cause(err, "invalid uuid for user ", index)
}
users = append(users, tuic.User{Name: user.Name, UUID: userUUID, Password: user.Password})
}
inbound := &TUIC{
myInboundAdapter: myInboundAdapter{
protocol: C.TypeTUIC,
network: []string{N.NetworkUDP},
ctx: ctx,
router: router,
logger: logger,
tag: tag,
listenOptions: options.ListenOptions,
},
}
server, err := tuic.NewServer(tuic.ServerOptions{
Context: ctx,
Logger: logger,
TLSConfig: rawConfig,
Users: users,
CongestionControl: options.CongestionControl,
AuthTimeout: time.Duration(options.AuthTimeout),
ZeroRTTHandshake: options.ZeroRTTHandshake,
Heartbeat: time.Duration(options.Heartbeat),
Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil),
})
if err != nil {
return nil, err
}
inbound.server = server
return inbound, nil
}
func (h *TUIC) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
ctx = log.ContextWithNewID(ctx)
h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination)
metadata = h.createMetadata(conn, metadata)
metadata.User, _ = auth.UserFromContext[string](ctx)
return h.router.RouteConnection(ctx, conn, metadata)
}
func (h *TUIC) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
ctx = log.ContextWithNewID(ctx)
metadata = h.createPacketMetadata(conn, metadata)
metadata.User, _ = auth.UserFromContext[string](ctx)
h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
return h.router.RoutePacketConnection(ctx, conn, metadata)
}
func (h *TUIC) Start() error {
packetConn, err := h.myInboundAdapter.ListenUDP()
if err != nil {
return err
}
return h.server.Start(packetConn)
}
func (h *TUIC) Close() error {
return common.Close(
&h.myInboundAdapter,
common.PtrOrNil(h.server),
)
}

16
inbound/tuic_stub.go Normal file
View File

@ -0,0 +1,16 @@
//go:build !with_quic
package inbound
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) {
return nil, C.ErrQUICNotIncluded
}

View File

@ -86,6 +86,7 @@ nav:
- Hysteria: configuration/inbound/hysteria.md
- ShadowTLS: configuration/inbound/shadowtls.md
- VLESS: configuration/inbound/vless.md
- TUIC: configuration/inbound/tuic.md
- Tun: configuration/inbound/tun.md
- Redirect: configuration/inbound/redirect.md
- TProxy: configuration/inbound/tproxy.md
@ -103,6 +104,7 @@ nav:
- ShadowTLS: configuration/outbound/shadowtls.md
- ShadowsocksR: configuration/outbound/shadowsocksr.md
- VLESS: configuration/outbound/vless.md
- TUIC: configuration/outbound/tuic.md
- Tor: configuration/outbound/tor.md
- SSH: configuration/outbound/ssh.md
- DNS: configuration/outbound/dns.md
@ -120,7 +122,6 @@ nav:
- Shadowsocks: examples/shadowsocks.md
- ShadowTLS: examples/shadowtls.md
- Clash API: examples/clash-api.md
- WireGuard Direct: examples/wireguard-direct.md
- FakeIP: examples/fakeip.md
- Contributing:
- contributing/index.md

View File

@ -31,7 +31,7 @@ type Service struct {
clockOffset time.Duration
}
func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) *Service {
func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) (*Service, error) {
ctx, cancel := common.ContextWithCancelCause(ctx)
server := options.ServerOptions.Build()
if server.Port == 0 {
@ -43,15 +43,19 @@ func NewService(ctx context.Context, router adapter.Router, logger logger.Logger
} else {
interval = 30 * time.Minute
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
return &Service{
ctx: ctx,
cancel: cancel,
server: server,
writeToSystem: options.WriteToSystem,
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
logger: logger,
ticker: time.NewTicker(interval),
}
}, nil
}
func (s *Service) Start() error {

View File

@ -23,6 +23,7 @@ type _Inbound struct {
HysteriaOptions HysteriaInboundOptions `json:"-"`
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
VLESSOptions VLESSInboundOptions `json:"-"`
TUICOptions TUICInboundOptions `json:"-"`
}
type Inbound _Inbound
@ -58,6 +59,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
v = h.ShadowTLSOptions
case C.TypeVLESS:
v = h.VLESSOptions
case C.TypeTUIC:
v = h.TUICOptions
default:
return nil, E.New("unknown inbound type: ", h.Type)
}
@ -99,6 +102,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
v = &h.ShadowTLSOptions
case C.TypeVLESS:
v = &h.VLESSOptions
case C.TypeTUIC:
v = &h.TUICOptions
default:
return E.New("unknown inbound type: ", h.Type)
}
@ -120,6 +125,7 @@ type ListenOptions struct {
Listen *ListenAddress `json:"listen,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
UDPTimeout int64 `json:"udp_timeout,omitempty"`

View File

@ -23,6 +23,7 @@ type _Outbound struct {
ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"`
ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"`
VLESSOptions VLESSOutboundOptions `json:"-"`
TUICOptions TUICOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
URLTestOptions URLTestOutboundOptions `json:"-"`
}
@ -60,6 +61,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
v = h.ShadowsocksROptions
case C.TypeVLESS:
v = h.VLESSOptions
case C.TypeTUIC:
v = h.TUICOptions
case C.TypeSelector:
v = h.SelectorOptions
case C.TypeURLTest:
@ -105,6 +108,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
v = &h.ShadowsocksROptions
case C.TypeVLESS:
v = &h.VLESSOptions
case C.TypeTUIC:
v = &h.TUICOptions
case C.TypeSelector:
v = &h.SelectorOptions
case C.TypeURLTest:
@ -129,6 +134,7 @@ type DialerOptions struct {
ReuseAddr bool `json:"reuse_addr,omitempty"`
ConnectTimeout Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
TCPMultiPath bool `json:"tcp_multi_path,omitempty"`
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`

30
option/tuic.go Normal file
View File

@ -0,0 +1,30 @@
package option
type TUICInboundOptions struct {
ListenOptions
Users []TUICUser `json:"users,omitempty"`
CongestionControl string `json:"congestion_control,omitempty"`
AuthTimeout Duration `json:"auth_timeout,omitempty"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat Duration `json:"heartbeat,omitempty"`
TLS *InboundTLSOptions `json:"tls,omitempty"`
}
type TUICUser struct {
Name string `json:"name,omitempty"`
UUID string `json:"uuid,omitempty"`
Password string `json:"password,omitempty"`
}
type TUICOutboundOptions struct {
DialerOptions
ServerOptions
UUID string `json:"uuid,omitempty"`
Password string `json:"password,omitempty"`
CongestionControl string `json:"congestion_control,omitempty"`
UDPRelayMode string `json:"udp_relay_mode,omitempty"`
ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"`
Heartbeat Duration `json:"heartbeat,omitempty"`
Network NetworkList `json:"network,omitempty"`
TLS *OutboundTLSOptions `json:"tls,omitempty"`
}

View File

@ -98,9 +98,9 @@ func (l *Listable[T]) UnmarshalJSON(content []byte) error {
return nil
}
var singleItem T
err = json.Unmarshal(content, &singleItem)
if err != nil {
return err
newError := json.Unmarshal(content, &singleItem)
if newError != nil {
return E.Errors(err, newError)
}
*l = []T{singleItem}
return nil

View File

@ -51,6 +51,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return NewShadowsocksR(ctx, router, logger, tag, options.ShadowsocksROptions)
case C.TypeVLESS:
return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
case C.TypeTUIC:
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
case C.TypeSelector:
return NewSelector(router, logger, tag, options.SelectorOptions)
case C.TypeURLTest:

View File

@ -38,6 +38,10 @@ type Direct struct {
func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) {
options.UDPFragmentDefault = true
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &Direct{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeDirect,
@ -49,7 +53,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti
},
domainStrategy: dns.DomainStrategy(options.DomainStrategy),
fallbackDelay: time.Duration(options.FallbackDelay),
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
proxyProto: options.ProxyProtocol,
}
if options.ProxyProtocol > 2 {

View File

@ -26,7 +26,11 @@ type HTTP struct {
}
func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
detour, err := tls.NewDialerFromOptions(router, dialer.New(router, options.DialerOptions), options.Server, common.PtrValueOrDefault(options.TLS))
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
detour, err := tls.NewDialerFromOptions(router, outboundDialer, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}

View File

@ -117,6 +117,10 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
if down < hysteria.MinSpeedBPS {
return nil, E.New("invalid down speed")
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
return &Hysteria{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeHysteria,
@ -127,7 +131,7 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL
dependencies: withDialerDependency(options.DialerOptions),
},
ctx: ctx,
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
tlsConfig: tlsConfig,
quicConfig: quicConfig,
@ -214,7 +218,7 @@ func (h *Hysteria) offerNew(ctx context.Context) (quic.Connection, error) {
func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
for {
packet, err := conn.ReceiveMessage()
packet, err := conn.ReceiveMessage(h.ctx)
if err != nil {
return
}
@ -241,9 +245,9 @@ func (h *Hysteria) udpRecvLoop(conn quic.Connection) {
}
}
func (h *Hysteria) InterfaceUpdated() error {
func (h *Hysteria) InterfaceUpdated() {
h.Close()
return nil
return
}
func (h *Hysteria) Close() error {

View File

@ -39,6 +39,10 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
if err != nil {
return nil, err
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &Shadowsocks{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeShadowsocks,
@ -48,7 +52,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
method: method,
serverAddr: options.ServerOptions.Build(),
}
@ -129,11 +133,11 @@ func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn
return NewPacketConnection(ctx, h, conn, metadata)
}
func (h *Shadowsocks) InterfaceUpdated() error {
func (h *Shadowsocks) InterfaceUpdated() {
if h.multiplexDialer != nil {
h.multiplexDialer.Reset()
}
return nil
return
}
func (h *Shadowsocks) Close() error {

View File

@ -37,6 +37,10 @@ type ShadowsocksR struct {
}
func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (*ShadowsocksR, error) {
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &ShadowsocksR{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeShadowsocksR,
@ -46,11 +50,10 @@ func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.Cont
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
}
var cipher string
var err error
switch options.Method {
case "none":
cipher = "dummy"

View File

@ -72,11 +72,15 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context
tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig)
}
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
client, err := shadowtls.NewClient(shadowtls.ClientConfig{
Version: options.Version,
Password: options.Password,
Server: options.ServerOptions.Build(),
Dialer: dialer.New(router, options.DialerOptions),
Dialer: outboundDialer,
TLSHandshake: tlsHandshakeFunc,
Logger: logger,
})

View File

@ -37,6 +37,10 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
if err != nil {
return nil, err
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &Socks{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeSOCKS,
@ -46,7 +50,7 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
client: socks.NewClient(dialer.New(router, options.DialerOptions), options.ServerOptions.Build(), version, options.Username, options.Password),
client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password),
resolve: version == socks.Version4,
}
uotOptions := common.PtrValueOrDefault(options.UDPOverTCPOptions)

View File

@ -44,6 +44,10 @@ type SSH struct {
}
func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (*SSH, error) {
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &SSH{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeSSH,
@ -54,7 +58,7 @@ func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger
dependencies: withDialerDependency(options.DialerOptions),
},
ctx: ctx,
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
user: options.User,
hostKeyAlgorithms: options.HostKeyAlgorithms,
@ -174,9 +178,9 @@ func (s *SSH) connect() (*ssh.Client, error) {
return client, nil
}
func (s *SSH) InterfaceUpdated() error {
func (s *SSH) InterfaceUpdated() {
common.Close(s.clientConn)
return nil
return
}
func (s *SSH) Close() error {

View File

@ -66,6 +66,10 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
}
startConf.TorrcFile = torrcFile
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
return &Tor{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeTor,
@ -76,7 +80,7 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger
dependencies: withDialerDependency(options.DialerOptions),
},
ctx: ctx,
proxy: NewProxyListener(ctx, logger, dialer.New(router, options.DialerOptions)),
proxy: NewProxyListener(ctx, logger, outboundDialer),
startConf: &startConf,
options: options.Options,
}, nil

View File

@ -33,6 +33,10 @@ type Trojan struct {
}
func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) {
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &Trojan{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeTrojan,
@ -42,11 +46,10 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
key: trojan.Key(options.Password),
}
var err error
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
@ -104,11 +107,11 @@ func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, met
return NewPacketConnection(ctx, h, conn, metadata)
}
func (h *Trojan) InterfaceUpdated() error {
func (h *Trojan) InterfaceUpdated() {
if h.multiplexDialer != nil {
h.multiplexDialer.Reset()
}
return nil
return
}
func (h *Trojan) Close() error {

127
outbound/tuic.go Normal file
View File

@ -0,0 +1,127 @@
//go:build with_quic
package outbound
import (
"context"
"net"
"os"
"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/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/tuic"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
)
var (
_ adapter.Outbound = (*TUIC)(nil)
_ adapter.InterfaceUpdateListener = (*TUIC)(nil)
)
type TUIC struct {
myOutboundAdapter
client *tuic.Client
}
func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (*TUIC, error) {
options.UDPFragmentDefault = true
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}
abstractTLSConfig, err := tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
tlsConfig, err := abstractTLSConfig.Config()
if err != nil {
return nil, err
}
userUUID, err := uuid.FromString(options.UUID)
if err != nil {
return nil, E.Cause(err, "invalid uuid")
}
var udpStream bool
switch options.UDPRelayMode {
case "native":
case "quic":
udpStream = true
}
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
client, err := tuic.NewClient(tuic.ClientOptions{
Context: ctx,
Dialer: outboundDialer,
ServerAddress: options.ServerOptions.Build(),
TLSConfig: tlsConfig,
UUID: userUUID,
Password: options.Password,
CongestionControl: options.CongestionControl,
UDPStream: udpStream,
ZeroRTTHandshake: options.ZeroRTTHandshake,
Heartbeat: time.Duration(options.Heartbeat),
})
if err != nil {
return nil, err
}
return &TUIC{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeTUIC,
network: options.Network.Build(),
router: router,
logger: logger,
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
client: client,
}, nil
}
func (h *TUIC) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
switch N.NetworkName(network) {
case N.NetworkTCP:
h.logger.InfoContext(ctx, "outbound connection to ", destination)
return h.client.DialConn(ctx, destination)
case N.NetworkUDP:
conn, err := h.ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return bufio.NewBindPacketConn(conn, destination), nil
default:
return nil, E.New("unsupported network: ", network)
}
}
func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
h.logger.InfoContext(ctx, "outbound packet connection to ", destination)
return h.client.ListenPacket(ctx)
}
func (h *TUIC) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
return NewConnection(ctx, h, conn, metadata)
}
func (h *TUIC) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return NewPacketConnection(ctx, h, conn, metadata)
}
func (h *TUIC) InterfaceUpdated() {
_ = h.client.CloseWithError(E.New("network changed"))
}
func (h *TUIC) Close() error {
return h.client.CloseWithError(os.ErrClosed)
}

16
outbound/tuic_stub.go Normal file
View File

@ -0,0 +1,16 @@
//go:build !with_quic
package outbound
import (
"context"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) {
return nil, C.ErrQUICNotIncluded
}

View File

@ -8,7 +8,6 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/sleep"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
@ -20,6 +19,7 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
var (
@ -138,9 +138,9 @@ func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, me
return NewPacketConnection(ctx, s, conn, metadata)
}
func (s *URLTest) InterfaceUpdated() error {
func (s *URLTest) InterfaceUpdated() {
go s.group.CheckOutbounds(true)
return nil
return
}
type URLTestGroup struct {
@ -153,7 +153,7 @@ type URLTestGroup struct {
tolerance uint16
history *urltest.HistoryStorage
checking atomic.Bool
sleepManager *sleep.Manager
pauseManager pause.Manager
access sync.Mutex
ticker *time.Ticker
@ -184,7 +184,7 @@ func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logg
tolerance: tolerance,
history: history,
close: make(chan struct{}),
sleepManager: service.PtrFromContext[sleep.Manager](ctx),
pauseManager: pause.ManagerFromContext(ctx),
}
}
@ -266,9 +266,7 @@ func (g *URLTestGroup) Fallback(used adapter.Outbound) []adapter.Outbound {
func (g *URLTestGroup) loopCheck() {
go g.CheckOutbounds(true)
for {
if g.sleepManager != nil {
<-g.sleepManager.Active()
}
g.pauseManager.WaitActive()
select {
case <-g.close:
return

View File

@ -36,6 +36,10 @@ type VLESS struct {
}
func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) {
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &VLESS{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeVLESS,
@ -45,10 +49,9 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
}
var err error
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
@ -123,11 +126,11 @@ func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, meta
return NewPacketConnection(ctx, h, conn, metadata)
}
func (h *VLESS) InterfaceUpdated() error {
func (h *VLESS) InterfaceUpdated() {
if h.multiplexDialer != nil {
h.multiplexDialer.Reset()
}
return nil
return
}
func (h *VLESS) Close() error {

View File

@ -35,6 +35,10 @@ type VMess struct {
}
func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (*VMess, error) {
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound := &VMess{
myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeVMess,
@ -44,10 +48,9 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
tag: tag,
dependencies: withDialerDependency(options.DialerOptions),
},
dialer: dialer.New(router, options.DialerOptions),
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
}
var err error
if options.TLS != nil {
outbound.tlsConfig, err = tls.NewClient(router, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
@ -98,11 +101,11 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg
return outbound, nil
}
func (h *VMess) InterfaceUpdated() error {
func (h *VMess) InterfaceUpdated() {
if h.multiplexDialer != nil {
h.multiplexDialer.Reset()
}
return nil
return
}
func (h *VMess) Close() error {

View File

@ -65,7 +65,11 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
connectAddr = options.ServerOptions.Build()
}
}
outbound.bind = wireguard.NewClientBind(ctx, outbound, dialer.New(router, options.DialerOptions), isConnect, connectAddr, reserved)
outboundDialer, err := dialer.New(router, options.DialerOptions)
if err != nil {
return nil, err
}
outbound.bind = wireguard.NewClientBind(ctx, outbound, outboundDialer, isConnect, connectAddr, reserved)
localPrefixes := common.Map(options.LocalAddress, option.ListenPrefix.Build)
if len(localPrefixes) == 0 {
return nil, E.New("missing local address")
@ -157,7 +161,6 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
mtu = 1408
}
var wireTunDevice wireguard.Device
var err error
if !options.SystemInterface && tun.WithGVisor {
wireTunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu)
} else {
@ -166,7 +169,7 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
wgDevice := device.NewDevice(wireTunDevice, outbound.bind, &device.Logger{
wgDevice := device.NewDevice(ctx, wireTunDevice, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
},
@ -186,9 +189,9 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
return outbound, nil
}
func (w *WireGuard) InterfaceUpdated() error {
func (w *WireGuard) InterfaceUpdated() {
w.bind.Reset()
return nil
return
}
func (w *WireGuard) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@ -2,6 +2,7 @@ package route
import (
"context"
"errors"
"net"
"net/netip"
"net/url"
@ -37,7 +38,10 @@ import (
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
serviceNTP "github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/common/uot"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
var _ adapter.Router = (*Router)(nil)
@ -78,6 +82,7 @@ type Router struct {
packageManager tun.PackageManager
processSearcher process.Searcher
timeService adapter.TimeService
pauseManager pause.Manager
clashServer adapter.ClashServer
v2rayServer adapter.V2RayServer
platformInterface platform.Interface
@ -109,6 +114,7 @@ func NewRouter(
autoDetectInterface: options.AutoDetectInterface,
defaultInterface: options.DefaultInterface,
defaultMark: options.DefaultMark,
pauseManager: pause.ManagerFromContext(ctx),
platformInterface: platformInterface,
}
router.dnsClient = dns.NewClient(dns.ClientOptions{
@ -260,30 +266,30 @@ func NewRouter(
return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute
})
if needInterfaceMonitor {
if !usePlatformDefaultInterfaceMonitor {
networkMonitor, err := tun.NewNetworkUpdateMonitor(router)
if err != os.ErrInvalid {
if err != nil {
return nil, err
}
router.networkMonitor = networkMonitor
networkMonitor.RegisterCallback(router.interfaceFinder.update)
interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, tun.DefaultInterfaceMonitorOptions{
OverrideAndroidVPN: options.OverrideAndroidVPN,
UnderNetworkExtension: platformInterface != nil && platformInterface.UnderNetworkExtension(),
})
if err != nil {
return nil, E.New("auto_detect_interface unsupported on current platform")
}
interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate)
router.interfaceMonitor = interfaceMonitor
if !usePlatformDefaultInterfaceMonitor {
networkMonitor, err := tun.NewNetworkUpdateMonitor(router.logger)
if !((err != nil && !needInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) {
if err != nil {
return nil, err
}
router.networkMonitor = networkMonitor
networkMonitor.RegisterCallback(func() {
_ = router.interfaceFinder.update()
})
interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{
OverrideAndroidVPN: options.OverrideAndroidVPN,
UnderNetworkExtension: platformInterface != nil && platformInterface.UnderNetworkExtension(),
})
if err != nil {
return nil, E.New("auto_detect_interface unsupported on current platform")
}
} else {
interfaceMonitor := platformInterface.CreateDefaultInterfaceMonitor(router)
interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate)
router.interfaceMonitor = interfaceMonitor
}
} else {
interfaceMonitor := platformInterface.CreateDefaultInterfaceMonitor(router.logger)
interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate)
router.interfaceMonitor = interfaceMonitor
}
needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess
@ -315,7 +321,12 @@ func NewRouter(
}
}
if ntpOptions.Enabled {
router.timeService = ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
timeService, err := ntp.NewService(ctx, router, logFactory.NewLogger("ntp"), ntpOptions)
if err != nil {
return nil, err
}
service.ContextWith[serviceNTP.TimeService](ctx, timeService)
router.timeService = timeService
}
return router, nil
}
@ -625,6 +636,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
if !loaded {
return E.New("missing fakeip context")
}
metadata.OriginDestination = metadata.Destination
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
@ -728,13 +740,12 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
}
metadata.Network = N.NetworkUDP
var originAddress M.Socksaddr
if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
if !loaded {
return E.New("missing fakeip context")
}
originAddress = metadata.Destination
metadata.OriginDestination = metadata.Destination
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
@ -748,7 +759,7 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn))
}*/
if metadata.InboundOptions.SniffEnabled {
if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() {
buffer := buf.NewPacket()
buffer.FullReset()
destination, err := conn.ReadPacket(buffer)
@ -756,6 +767,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
buffer.Release()
return err
}
if metadata.Destination.Addr.IsUnspecified() {
metadata.Destination = destination
}
sniffMetadata, _ := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage)
if sniffMetadata != nil {
metadata.Protocol = sniffMetadata.Protocol
@ -806,8 +820,8 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
}
}
if originAddress.IsValid() {
conn = fakeip.NewNATPacketConn(conn, originAddress, metadata.Destination)
if metadata.FakeIP {
conn = fakeip.NewNATPacketConn(conn, metadata.OriginDestination, metadata.Destination)
}
return detour.NewPacketConnection(ctx, conn, metadata)
}
@ -970,17 +984,23 @@ func (r *Router) NewError(ctx context.Context, err error) {
r.logger.ErrorContext(ctx, err)
}
func (r *Router) notifyNetworkUpdate(int) error {
if C.IsAndroid && r.platformInterface == nil {
var vpnStatus string
if r.interfaceMonitor.AndroidVPNEnabled() {
vpnStatus = "enabled"
} else {
vpnStatus = "disabled"
}
r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()), ", vpn ", vpnStatus)
func (r *Router) notifyNetworkUpdate(event int) {
if event == tun.EventNoRoute {
r.pauseManager.NetworkPause()
r.logger.Error("missing default interface")
} else {
r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()))
r.pauseManager.NetworkWake()
if C.IsAndroid && r.platformInterface == nil {
var vpnStatus string
if r.interfaceMonitor.AndroidVPNEnabled() {
vpnStatus = "enabled"
} else {
vpnStatus = "disabled"
}
r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()), ", vpn ", vpnStatus)
} else {
r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()))
}
}
conntrack.Close()
@ -988,13 +1008,10 @@ func (r *Router) notifyNetworkUpdate(int) error {
for _, outbound := range r.outbounds {
listener, isListener := outbound.(adapter.InterfaceUpdateListener)
if isListener {
err := listener.InterfaceUpdated()
if err != nil {
return err
}
listener.InterfaceUpdated()
}
}
return nil
return
}
func (r *Router) ResetNetwork() error {
@ -1003,10 +1020,7 @@ func (r *Router) ResetNetwork() error {
for _, outbound := range r.outbounds {
listener, isListener := outbound.(adapter.InterfaceUpdateListener)
if isListener {
err := listener.InterfaceUpdated()
if err != nil {
return err
}
listener.InterfaceUpdated()
}
}
return nil

View File

@ -38,6 +38,8 @@ const (
ImageShadowsocksR = "teddysun/shadowsocks-r:latest"
ImageXRayCore = "teddysun/xray:latest"
ImageShadowsocksLegacy = "mritd/shadowsocks:latest"
ImageTUICServer = ""
ImageTUICClient = ""
)
var allImages = []string{
@ -53,6 +55,8 @@ var allImages = []string{
ImageShadowsocksR,
ImageXRayCore,
ImageShadowsocksLegacy,
// ImageTUICServer,
// ImageTUICClient,
}
var localIP = netip.MustParseAddr("127.0.0.1")

View File

@ -0,0 +1,14 @@
{
"relay": {
"server": "127.0.0.1:10000",
"uuid": "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D",
"password": "tuic",
"certificates": [
"/etc/tuic/ca.pem"
]
},
"local": {
"server": "127.0.0.1:10001"
},
"log_level": "debug"
}

View File

@ -0,0 +1,9 @@
{
"server": "[::]:10000",
"users": {
"FE35D05B-8803-45C4-BAE6-723AD2CD5D3D": "tuic"
},
"certificate": "/etc/tuic/cert.pem",
"private_key": "/etc/tuic/key.pem",
"log_level": "debug"
}

View File

@ -10,7 +10,7 @@ require (
github.com/docker/docker v20.10.18+incompatible
github.com/docker/go-connections v0.4.0
github.com/gofrs/uuid/v5 v5.0.0
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a
github.com/sagernet/sing-shadowsocks v0.2.4
github.com/sagernet/sing-shadowsocks2 v0.1.3
github.com/spyzhov/ajson v0.7.1
@ -40,6 +40,7 @@ require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@ -64,25 +65,23 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/qtls-go1-20 v0.3.1 // indirect
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 // indirect
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 // indirect
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 // indirect
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 // indirect
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111 // indirect
github.com/sagernet/quic-go v0.0.0-20230811130919-d6f54a117913 // indirect
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 // indirect
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21 // indirect
github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/sing-tun v0.1.11 // indirect
github.com/sagernet/sing-tun v0.1.12-0.20230812113806-10d98f26797a // indirect
github.com/sagernet/sing-vmess v0.1.7 // indirect
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 // indirect
github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 // indirect
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 // indirect
github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e // indirect
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77 // indirect
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f // indirect
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect

View File

@ -53,6 +53,7 @@ github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -112,12 +113,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt1Jtp5vW2ohNvstvQffTOQ/s5vENuGXzdA+TM=
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
@ -126,26 +123,31 @@ github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTS
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111 h1:BjZBIgwzlIbRSijdGQYZf0CaqHY1ZEIOUqVEKEICU0U=
github.com/sagernet/quic-go v0.0.0-20230731012313-1327e4015111/go.mod h1:5rilP6WxqIl/4ypZbMjr+MK+STxuCEvO5yVtEyYNZ6g=
github.com/sagernet/quic-go v0.0.0-20230809023643-d720ed35ac2b h1:+hpCW1zw03nnJDx+5tF9ETb6bKl2VSftv4KMGZAHC2Q=
github.com/sagernet/quic-go v0.0.0-20230809023643-d720ed35ac2b/go.mod h1:w+nln6f/ZtyPpGbFxmgd5iYFVMmgS+gpD5hu5GAqC1I=
github.com/sagernet/quic-go v0.0.0-20230811130919-d6f54a117913/go.mod h1:w+nln6f/ZtyPpGbFxmgd5iYFVMmgS+gpD5hu5GAqC1I=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee h1:5MATgtWMh2TCAVMtQnC3UcVMympANU7zXEekctD29PY=
github.com/sagernet/sing v0.2.10-0.20230802105922-c6a69b4912ee/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a h1:b89t6Mjgk4rJ5lrNMnCzy1/J116XkhgdB3YNd9FHyF4=
github.com/sagernet/sing v0.2.10-0.20230807080248-4db0062caa0a/go.mod h1:9uOZwWkhT2Z2WldolLxX34s+1svAX4i4vvz5hy8u1MA=
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659 h1:1DAKccGNqTYJ8nsBR765FS0LVBVXfuFlFAHqKsGN3EI=
github.com/sagernet/sing-dns v0.1.9-0.20230731012726-ad50da89b659/go.mod h1:W7GHTZFS8RkoLI3bA2LFY27/0E+uoQESWtMFLepO/JA=
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21 h1:IQ7oBBKz+lwIqwI9IMStlQ9YSUu3eKJmNTip0aLbvOI=
github.com/sagernet/sing-mux v0.1.3-0.20230803070305-ea4a972acd21/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
github.com/sagernet/sing-mux v0.1.3-0.20230811111955-dc1639b5204c/go.mod h1:TKxqIvfQQgd36jp2tzsPavGjYTVZilV+atip1cssjIY=
github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
github.com/sagernet/sing-shadowsocks v0.2.4/go.mod h1:80fNKP0wnqlu85GZXV1H1vDPC/2t+dQbFggOw4XuFUM=
github.com/sagernet/sing-shadowsocks2 v0.1.3 h1:WXoLvCFi5JTFBRYorf1YePGYIQyJ/zbsBM6Fwbl5kGA=
github.com/sagernet/sing-shadowsocks2 v0.1.3/go.mod h1:DOhJc/cLeqRv0wuePrQso+iUmDxOnWF4eT/oMcRzYFw=
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/sing-tun v0.1.11 h1:wUfRQZ4eHk8suHkGKEFxjV5uXl3tfZhPm/v14/4lHvk=
github.com/sagernet/sing-tun v0.1.11/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230808120247-47ab78d303db h1:jOwG+7u4NtQVwXj5pFnGeNnDoa2cv83O5x4NLKN8y/c=
github.com/sagernet/sing-tun v0.1.12-0.20230808120247-47ab78d303db/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230811070056-38478f5fbcd2/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230812113214-bc5a1f835a28/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230812113806-10d98f26797a/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-vmess v0.1.7 h1:TM8FFLsXmlXH9XT8/oDgc6PC5BOzrg6OzyEe01is2r4=
github.com/sagernet/sing-vmess v0.1.7/go.mod h1:1qkC1L1T2sxnS/NuO6HU72S8TkltV+EXoKGR29m/Yss=
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
@ -158,6 +160,7 @@ github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e h1:7uw2njHFGE+V
github.com/sagernet/websocket v0.0.0-20220913015213-615516348b4e/go.mod h1:45TUl8+gH4SIKr4ykREbxKWTxkDlSzFENzctB1dVRRY=
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77 h1:g6QtRWQ2dKX7EQP++1JLNtw4C2TNxd4/ov8YUpOPOSo=
github.com/sagernet/wireguard-go v0.0.0-20230420044414-a7bac1754e77/go.mod h1:pJDdXzZIwJ+2vmnT0TKzmf8meeum+e2mTDSehw79eE0=
github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@ -176,6 +179,7 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
@ -204,6 +208,7 @@ golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N0
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@ -211,12 +216,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -227,7 +234,9 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -249,6 +258,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

178
test/tuic_test.go Normal file
View File

@ -0,0 +1,178 @@
package main
import (
"net/netip"
"testing"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/gofrs/uuid/v5"
)
func TestTUICSelf(t *testing.T) {
t.Run("self", func(t *testing.T) {
testTUICSelf(t, false, false)
})
t.Run("self-udp-stream", func(t *testing.T) {
testTUICSelf(t, true, false)
})
t.Run("self-early", func(t *testing.T) {
testTUICSelf(t, false, true)
})
}
func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
var udpRelayMode string
if udpStream {
udpRelayMode = "quic"
}
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeMixed,
Tag: "mixed-in",
MixedOptions: option.HTTPMixedInboundOptions{
ListenOptions: option.ListenOptions{
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
ListenPort: clientPort,
},
},
},
{
Type: C.TypeTUIC,
TUICOptions: option.TUICInboundOptions{
ListenOptions: option.ListenOptions{
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
ListenPort: serverPort,
},
Users: []option.TUICUser{{
UUID: uuid.Nil.String(),
}},
ZeroRTTHandshake: zeroRTTHandshake,
TLS: &option.InboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
KeyPath: keyPem,
},
},
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeDirect,
},
{
Type: C.TypeTUIC,
Tag: "tuic-out",
TUICOptions: option.TUICOutboundOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: serverPort,
},
UUID: uuid.Nil.String(),
UDPRelayMode: udpRelayMode,
ZeroRTTHandshake: zeroRTTHandshake,
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
},
},
},
},
Route: &option.RouteOptions{
Rules: []option.Rule{
{
DefaultOptions: option.DefaultRule{
Inbound: []string{"mixed-in"},
Outbound: "tuic-out",
},
},
},
},
})
testSuit(t, clientPort, testPort)
}
func TestTUICInbound(t *testing.T) {
caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeTUIC,
TUICOptions: option.TUICInboundOptions{
ListenOptions: option.ListenOptions{
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
ListenPort: serverPort,
},
Users: []option.TUICUser{{
UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D",
Password: "tuic",
}},
TLS: &option.InboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
KeyPath: keyPem,
},
},
},
},
})
startDockerContainer(t, DockerOptions{
Image: ImageTUICClient,
Ports: []uint16{serverPort, clientPort},
Bind: map[string]string{
"tuic-client.json": "/etc/tuic/config.json",
caPem: "/etc/tuic/ca.pem",
},
})
}
func TestTUICOutbound(t *testing.T) {
_, certPem, keyPem := createSelfSignedCertificate(t, "example.org")
startDockerContainer(t, DockerOptions{
Image: ImageTUICServer,
Ports: []uint16{testPort},
Bind: map[string]string{
"tuic-server.json": "/etc/tuic/config.json",
certPem: "/etc/tuic/cert.pem",
keyPem: "/etc/tuic/key.pem",
},
})
startInstance(t, option.Options{
Inbounds: []option.Inbound{
{
Type: C.TypeMixed,
MixedOptions: option.HTTPMixedInboundOptions{
ListenOptions: option.ListenOptions{
Listen: option.NewListenAddress(netip.IPv4Unspecified()),
ListenPort: clientPort,
},
},
},
},
Outbounds: []option.Outbound{
{
Type: C.TypeTUIC,
TUICOptions: option.TUICOutboundOptions{
ServerOptions: option.ServerOptions{
Server: "127.0.0.1",
ServerPort: serverPort,
},
UUID: "FE35D05B-8803-45C4-BAE6-723AD2CD5D3D",
Password: "tuic",
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: "example.org",
CertificatePath: certPem,
},
},
},
},
})
testSuit(t, clientPort, testPort)
}

View File

@ -162,8 +162,11 @@ func (t *Transport) updateServers() error {
}
}
func (t *Transport) interfaceUpdated(int) error {
return t.updateServers()
func (t *Transport) interfaceUpdated(int) {
err := t.updateServers()
if err != nil {
t.logger.Error("update servers: ", err)
}
}
func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) error {
@ -245,10 +248,10 @@ func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Ad
}), ","), "]")
}
serverDialer := dialer.NewDefault(t.router, option.DialerOptions{
serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{
BindInterface: iface.Name,
UDPFragmentDefault: true,
})
}))
var transports []dns.Transport
for _, serverAddr := range serverAddrs {
serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53})

View File

@ -34,6 +34,9 @@ func (s *MemoryStorage) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) err
return nil
}
func (s *MemoryStorage) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
}
func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error {
s.addressAccess.Lock()
s.domainAccess.Lock()

View File

@ -99,6 +99,12 @@ func (s *Store) Create(domain string, isIPv6 bool) (netip.Addr, error) {
address = nextAddress
}
s.storage.FakeIPStoreAsync(address, domain, s.logger)
s.storage.FakeIPSaveMetadataAsync(&adapter.FakeIPMetadata{
Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range,
Inet4Current: s.inet4Current,
Inet6Current: s.inet6Current,
})
return address, nil
}

10
transport/tuic/address.go Normal file
View File

@ -0,0 +1,10 @@
package tuic
import M "github.com/sagernet/sing/common/metadata"
var addressSerializer = M.NewSerializer(
M.AddressFamilyByte(0x00, M.AddressFamilyFqdn),
M.AddressFamilyByte(0x01, M.AddressFamilyIPv4),
M.AddressFamilyByte(0x02, M.AddressFamilyIPv6),
M.AddressFamilyByte(0xff, M.AddressFamilyEmpty),
)

322
transport/tuic/client.go Normal file
View File

@ -0,0 +1,322 @@
package tuic
import (
"context"
"crypto/tls"
"io"
"net"
"os"
"runtime"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/common/baderror"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
)
type ClientOptions struct {
Context context.Context
Dialer N.Dialer
ServerAddress M.Socksaddr
TLSConfig *tls.Config
UUID uuid.UUID
Password string
CongestionControl string
UDPStream bool
ZeroRTTHandshake bool
Heartbeat time.Duration
}
type Client struct {
ctx context.Context
dialer N.Dialer
serverAddr M.Socksaddr
tlsConfig *tls.Config
quicConfig *quic.Config
uuid uuid.UUID
password string
congestionControl string
udpStream bool
zeroRTTHandshake bool
heartbeat time.Duration
connAccess sync.RWMutex
conn *clientQUICConnection
}
func NewClient(options ClientOptions) (*Client, error) {
if options.Heartbeat == 0 {
options.Heartbeat = 10 * time.Second
}
quicConfig := &quic.Config{
DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"),
MaxDatagramFrameSize: 1400,
EnableDatagrams: true,
MaxIncomingUniStreams: 1 << 60,
}
switch options.CongestionControl {
case "":
options.CongestionControl = "cubic"
case "cubic", "new_reno", "bbr":
default:
return nil, E.New("unknown congestion control algorithm: ", options.CongestionControl)
}
return &Client{
ctx: options.Context,
dialer: options.Dialer,
serverAddr: options.ServerAddress,
tlsConfig: options.TLSConfig,
quicConfig: quicConfig,
uuid: options.UUID,
password: options.Password,
congestionControl: options.CongestionControl,
udpStream: options.UDPStream,
zeroRTTHandshake: options.ZeroRTTHandshake,
heartbeat: options.Heartbeat,
}, nil
}
func (c *Client) offer(ctx context.Context) (*clientQUICConnection, error) {
conn := c.conn
if conn != nil && conn.active() {
return conn, nil
}
c.connAccess.Lock()
defer c.connAccess.Unlock()
conn = c.conn
if conn != nil && conn.active() {
return conn, nil
}
conn, err := c.offerNew(ctx)
if err != nil {
return nil, err
}
return conn, nil
}
func (c *Client) offerNew(ctx context.Context) (*clientQUICConnection, error) {
udpConn, err := c.dialer.DialContext(ctx, "udp", c.serverAddr)
if err != nil {
return nil, err
}
var quicConn quic.Connection
if c.zeroRTTHandshake {
quicConn, err = quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
} else {
quicConn, err = quic.Dial(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), c.tlsConfig, c.quicConfig)
}
if err != nil {
udpConn.Close()
return nil, E.Cause(err, "open connection")
}
setCongestion(c.ctx, quicConn, c.congestionControl)
conn := &clientQUICConnection{
quicConn: quicConn,
rawConn: udpConn,
connDone: make(chan struct{}),
udpConnMap: make(map[uint16]*udpPacketConn),
}
go func() {
hErr := c.clientHandshake(quicConn)
if hErr != nil {
conn.closeWithError(hErr)
}
}()
if c.udpStream {
go c.loopUniStreams(conn)
}
go c.loopMessages(conn)
go c.loopHeartbeats(conn)
c.conn = conn
return conn, nil
}
func (c *Client) clientHandshake(conn quic.Connection) error {
authStream, err := conn.OpenUniStream()
if err != nil {
return err
}
defer authStream.Close()
handshakeState := conn.ConnectionState().TLS
tuicAuthToken, err := handshakeState.ExportKeyingMaterial(string(c.uuid[:]), []byte(c.password), 32)
if err != nil {
return err
}
authRequest := buf.NewSize(AuthenticateLen)
authRequest.WriteByte(Version)
authRequest.WriteByte(CommandAuthenticate)
authRequest.Write(c.uuid[:])
authRequest.Write(tuicAuthToken)
return common.Error(authStream.Write(authRequest.Bytes()))
}
func (c *Client) loopHeartbeats(conn *clientQUICConnection) {
ticker := time.NewTicker(c.heartbeat)
defer ticker.Stop()
for {
select {
case <-conn.connDone:
return
case <-ticker.C:
err := conn.quicConn.SendMessage([]byte{Version, CommandHeartbeat})
if err != nil {
conn.closeWithError(E.Cause(err, "send heartbeat"))
}
}
}
}
func (c *Client) DialConn(ctx context.Context, destination M.Socksaddr) (net.Conn, error) {
conn, err := c.offer(ctx)
if err != nil {
return nil, err
}
stream, err := conn.quicConn.OpenStream()
if err != nil {
return nil, err
}
return &clientConn{
parent: conn,
stream: stream,
destination: destination,
}, nil
}
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
conn, err := c.offer(ctx)
if err != nil {
return nil, err
}
var sessionID uint16
clientPacketConn := newUDPPacketConn(ctx, conn.quicConn, c.udpStream, false, func() {
conn.udpAccess.Lock()
delete(conn.udpConnMap, sessionID)
conn.udpAccess.Unlock()
})
conn.udpAccess.Lock()
sessionID = conn.udpSessionID
conn.udpSessionID++
conn.udpConnMap[sessionID] = clientPacketConn
conn.udpAccess.Unlock()
clientPacketConn.sessionID = sessionID
return clientPacketConn, nil
}
func (c *Client) CloseWithError(err error) error {
conn := c.conn
if conn != nil {
conn.closeWithError(err)
}
return nil
}
type clientQUICConnection struct {
quicConn quic.Connection
rawConn io.Closer
closeOnce sync.Once
connDone chan struct{}
connErr error
udpAccess sync.RWMutex
udpConnMap map[uint16]*udpPacketConn
udpSessionID uint16
}
func (c *clientQUICConnection) active() bool {
select {
case <-c.quicConn.Context().Done():
return false
default:
}
select {
case <-c.connDone:
return false
default:
}
return true
}
func (c *clientQUICConnection) closeWithError(err error) {
c.closeOnce.Do(func() {
c.connErr = err
close(c.connDone)
_ = c.quicConn.CloseWithError(0, "")
_ = c.rawConn.Close()
})
}
type clientConn struct {
parent *clientQUICConnection
stream quic.Stream
destination M.Socksaddr
requestWritten bool
}
func (c *clientConn) Read(b []byte) (n int, err error) {
n, err = c.stream.Read(b)
return n, baderror.WrapQUIC(err)
}
func (c *clientConn) Write(b []byte) (n int, err error) {
if !c.requestWritten {
request := buf.NewSize(2 + addressSerializer.AddrPortLen(c.destination) + len(b))
request.WriteByte(Version)
request.WriteByte(CommandConnect)
addressSerializer.WriteAddrPort(request, c.destination)
request.Write(b)
_, err = c.stream.Write(request.Bytes())
if err != nil {
c.parent.closeWithError(E.Cause(err, "create new connection"))
return 0, baderror.WrapQUIC(err)
}
c.requestWritten = true
return len(b), nil
}
n, err = c.stream.Write(b)
return n, baderror.WrapQUIC(err)
}
func (c *clientConn) Close() error {
stream := c.stream
if stream == nil {
return nil
}
stream.CancelRead(0)
return stream.Close()
}
func (c *clientConn) LocalAddr() net.Addr {
return M.Socksaddr{}
}
func (c *clientConn) RemoteAddr() net.Addr {
return c.destination
}
func (c *clientConn) SetDeadline(t time.Time) error {
if c.stream == nil {
return os.ErrInvalid
}
return c.stream.SetDeadline(t)
}
func (c *clientConn) SetReadDeadline(t time.Time) error {
if c.stream == nil {
return os.ErrInvalid
}
return c.stream.SetReadDeadline(t)
}
func (c *clientConn) SetWriteDeadline(t time.Time) error {
if c.stream == nil {
return os.ErrInvalid
}
return c.stream.SetWriteDeadline(t)
}

View File

@ -0,0 +1,110 @@
package tuic
import (
"io"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
)
func (c *Client) loopMessages(conn *clientQUICConnection) {
for {
message, err := conn.quicConn.ReceiveMessage(c.ctx)
if err != nil {
conn.closeWithError(E.Cause(err, "receive message"))
return
}
go func() {
hErr := c.handleMessage(conn, message)
if hErr != nil {
conn.closeWithError(E.Cause(hErr, "handle message"))
}
}()
}
}
func (c *Client) handleMessage(conn *clientQUICConnection, data []byte) error {
if len(data) < 2 {
return E.New("invalid message")
}
if data[0] != Version {
return E.New("unknown version ", data[0])
}
switch data[1] {
case CommandPacket:
message := udpMessagePool.Get().(*udpMessage)
err := decodeUDPMessage(message, data[2:])
if err != nil {
message.release()
return E.Cause(err, "decode UDP message")
}
conn.handleUDPMessage(message)
return nil
case CommandHeartbeat:
return nil
default:
return E.New("unknown command ", data[0])
}
}
func (c *Client) loopUniStreams(conn *clientQUICConnection) {
for {
stream, err := conn.quicConn.AcceptUniStream(c.ctx)
if err != nil {
conn.closeWithError(E.Cause(err, "handle uni stream"))
return
}
go func() {
hErr := c.handleUniStream(conn, stream)
if hErr != nil {
conn.closeWithError(hErr)
}
}()
}
}
func (c *Client) handleUniStream(conn *clientQUICConnection, stream quic.ReceiveStream) error {
defer stream.CancelRead(0)
buffer := buf.NewPacket()
defer buffer.Release()
_, err := buffer.ReadAtLeastFrom(stream, 2)
if err != nil {
return err
}
version, _ := buffer.ReadByte()
if version != Version {
return E.New("unknown version ", version)
}
command, _ := buffer.ReadByte()
if command != CommandPacket {
return E.New("unknown command ", command)
}
reader := io.MultiReader(bufio.NewCachedReader(stream, buffer), stream)
message := udpMessagePool.Get().(*udpMessage)
err = readUDPMessage(message, reader)
if err != nil {
message.release()
return err
}
conn.handleUDPMessage(message)
return nil
}
func (c *clientQUICConnection) handleUDPMessage(message *udpMessage) {
c.udpAccess.RLock()
udpConn, loaded := c.udpConnMap[message.sessionID]
c.udpAccess.RUnlock()
if !loaded {
message.releaseMessage()
return
}
select {
case <-udpConn.ctx.Done():
message.releaseMessage()
return
default:
}
udpConn.inputPacket(message)
}

View File

@ -0,0 +1,46 @@
package tuic
import (
"context"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/transport/tuic/congestion"
"github.com/sagernet/sing/common/ntp"
)
func setCongestion(ctx context.Context, connection quic.Connection, congestionName string) {
timeFunc := ntp.TimeFuncFromContext(ctx)
if timeFunc == nil {
timeFunc = time.Now
}
switch congestionName {
case "cubic":
connection.SetCongestionControl(
congestion.NewCubicSender(
congestion.DefaultClock{TimeFunc: timeFunc},
congestion.GetInitialPacketSize(connection.RemoteAddr()),
false,
nil,
),
)
case "new_reno":
connection.SetCongestionControl(
congestion.NewCubicSender(
congestion.DefaultClock{TimeFunc: timeFunc},
congestion.GetInitialPacketSize(connection.RemoteAddr()),
true,
nil,
),
)
case "bbr":
connection.SetCongestionControl(
congestion.NewBBRSender(
congestion.DefaultClock{},
congestion.GetInitialPacketSize(connection.RemoteAddr()),
congestion.InitialCongestionWindow*congestion.InitialMaxDatagramSize,
congestion.DefaultBBRMaxCongestionWindow*congestion.InitialMaxDatagramSize,
),
)
}
}

View File

@ -0,0 +1,3 @@
# congestion
mod from https://github.com/MetaCubeX/Clash.Meta/tree/53f9e1ee7104473da2b4ff5da29965563084482d/transport/tuic/congestion

View File

@ -0,0 +1,25 @@
package congestion
import (
"math"
"time"
"github.com/sagernet/quic-go/congestion"
)
// Bandwidth of a connection
type Bandwidth uint64
const infBandwidth Bandwidth = math.MaxUint64
const (
// BitsPerSecond is 1 bit per second
BitsPerSecond Bandwidth = 1
// BytesPerSecond is 1 byte per second
BytesPerSecond = 8 * BitsPerSecond
)
// BandwidthFromDelta calculates the bandwidth from a number of bytes and a time delta
func BandwidthFromDelta(bytes congestion.ByteCount, delta time.Duration) Bandwidth {
return Bandwidth(bytes) * Bandwidth(time.Second) / Bandwidth(delta) * BytesPerSecond
}

View File

@ -0,0 +1,374 @@
package congestion
import (
"math"
"time"
"github.com/sagernet/quic-go/congestion"
)
var InfiniteBandwidth = Bandwidth(math.MaxUint64)
// SendTimeState is a subset of ConnectionStateOnSentPacket which is returned
// to the caller when the packet is acked or lost.
type SendTimeState struct {
// Whether other states in this object is valid.
isValid bool
// Whether the sender is app limited at the time the packet was sent.
// App limited bandwidth sample might be artificially low because the sender
// did not have enough data to send in order to saturate the link.
isAppLimited bool
// Total number of sent bytes at the time the packet was sent.
// Includes the packet itself.
totalBytesSent congestion.ByteCount
// Total number of acked bytes at the time the packet was sent.
totalBytesAcked congestion.ByteCount
// Total number of lost bytes at the time the packet was sent.
totalBytesLost congestion.ByteCount
}
// ConnectionStateOnSentPacket represents the information about a sent packet
// and the state of the connection at the moment the packet was sent,
// specifically the information about the most recently acknowledged packet at
// that moment.
type ConnectionStateOnSentPacket struct {
packetNumber congestion.PacketNumber
// Time at which the packet is sent.
sendTime time.Time
// Size of the packet.
size congestion.ByteCount
// The value of |totalBytesSentAtLastAckedPacket| at the time the
// packet was sent.
totalBytesSentAtLastAckedPacket congestion.ByteCount
// The value of |lastAckedPacketSentTime| at the time the packet was
// sent.
lastAckedPacketSentTime time.Time
// The value of |lastAckedPacketAckTime| at the time the packet was
// sent.
lastAckedPacketAckTime time.Time
// Send time states that are returned to the congestion controller when the
// packet is acked or lost.
sendTimeState SendTimeState
}
// BandwidthSample
type BandwidthSample struct {
// The bandwidth at that particular sample. Zero if no valid bandwidth sample
// is available.
bandwidth Bandwidth
// The RTT measurement at this particular sample. Zero if no RTT sample is
// available. Does not correct for delayed ack time.
rtt time.Duration
// States captured when the packet was sent.
stateAtSend SendTimeState
}
func NewBandwidthSample() *BandwidthSample {
return &BandwidthSample{
// FIXME: the default value of original code is zero.
rtt: InfiniteRTT,
}
}
// BandwidthSampler keeps track of sent and acknowledged packets and outputs a
// bandwidth sample for every packet acknowledged. The samples are taken for
// individual packets, and are not filtered; the consumer has to filter the
// bandwidth samples itself. In certain cases, the sampler will locally severely
// underestimate the bandwidth, hence a maximum filter with a size of at least
// one RTT is recommended.
//
// This class bases its samples on the slope of two curves: the number of bytes
// sent over time, and the number of bytes acknowledged as received over time.
// It produces a sample of both slopes for every packet that gets acknowledged,
// based on a slope between two points on each of the corresponding curves. Note
// that due to the packet loss, the number of bytes on each curve might get
// further and further away from each other, meaning that it is not feasible to
// compare byte values coming from different curves with each other.
//
// The obvious points for measuring slope sample are the ones corresponding to
// the packet that was just acknowledged. Let us denote them as S_1 (point at
// which the current packet was sent) and A_1 (point at which the current packet
// was acknowledged). However, taking a slope requires two points on each line,
// so estimating bandwidth requires picking a packet in the past with respect to
// which the slope is measured.
//
// For that purpose, BandwidthSampler always keeps track of the most recently
// acknowledged packet, and records it together with every outgoing packet.
// When a packet gets acknowledged (A_1), it has not only information about when
// it itself was sent (S_1), but also the information about the latest
// acknowledged packet right before it was sent (S_0 and A_0).
//
// Based on that data, send and ack rate are estimated as:
//
// send_rate = (bytes(S_1) - bytes(S_0)) / (time(S_1) - time(S_0))
// ack_rate = (bytes(A_1) - bytes(A_0)) / (time(A_1) - time(A_0))
//
// Here, the ack rate is intuitively the rate we want to treat as bandwidth.
// However, in certain cases (e.g. ack compression) the ack rate at a point may
// end up higher than the rate at which the data was originally sent, which is
// not indicative of the real bandwidth. Hence, we use the send rate as an upper
// bound, and the sample value is
//
// rate_sample = min(send_rate, ack_rate)
//
// An important edge case handled by the sampler is tracking the app-limited
// samples. There are multiple meaning of "app-limited" used interchangeably,
// hence it is important to understand and to be able to distinguish between
// them.
//
// Meaning 1: connection state. The connection is said to be app-limited when
// there is no outstanding data to send. This means that certain bandwidth
// samples in the future would not be an accurate indication of the link
// capacity, and it is important to inform consumer about that. Whenever
// connection becomes app-limited, the sampler is notified via OnAppLimited()
// method.
//
// Meaning 2: a phase in the bandwidth sampler. As soon as the bandwidth
// sampler becomes notified about the connection being app-limited, it enters
// app-limited phase. In that phase, all *sent* packets are marked as
// app-limited. Note that the connection itself does not have to be
// app-limited during the app-limited phase, and in fact it will not be
// (otherwise how would it send packets?). The boolean flag below indicates
// whether the sampler is in that phase.
//
// Meaning 3: a flag on the sent packet and on the sample. If a sent packet is
// sent during the app-limited phase, the resulting sample related to the
// packet will be marked as app-limited.
//
// With the terminology issue out of the way, let us consider the question of
// what kind of situation it addresses.
//
// Consider a scenario where we first send packets 1 to 20 at a regular
// bandwidth, and then immediately run out of data. After a few seconds, we send
// packets 21 to 60, and only receive ack for 21 between sending packets 40 and
// 41. In this case, when we sample bandwidth for packets 21 to 40, the S_0/A_0
// we use to compute the slope is going to be packet 20, a few seconds apart
// from the current packet, hence the resulting estimate would be extremely low
// and not indicative of anything. Only at packet 41 the S_0/A_0 will become 21,
// meaning that the bandwidth sample would exclude the quiescence.
//
// Based on the analysis of that scenario, we implement the following rule: once
// OnAppLimited() is called, all sent packets will produce app-limited samples
// up until an ack for a packet that was sent after OnAppLimited() was called.
// Note that while the scenario above is not the only scenario when the
// connection is app-limited, the approach works in other cases too.
type BandwidthSampler struct {
// The total number of congestion controlled bytes sent during the connection.
totalBytesSent congestion.ByteCount
// The total number of congestion controlled bytes which were acknowledged.
totalBytesAcked congestion.ByteCount
// The total number of congestion controlled bytes which were lost.
totalBytesLost congestion.ByteCount
// The value of |totalBytesSent| at the time the last acknowledged packet
// was sent. Valid only when |lastAckedPacketSentTime| is valid.
totalBytesSentAtLastAckedPacket congestion.ByteCount
// The time at which the last acknowledged packet was sent. Set to
// QuicTime::Zero() if no valid timestamp is available.
lastAckedPacketSentTime time.Time
// The time at which the most recent packet was acknowledged.
lastAckedPacketAckTime time.Time
// The most recently sent packet.
lastSendPacket congestion.PacketNumber
// Indicates whether the bandwidth sampler is currently in an app-limited
// phase.
isAppLimited bool
// The packet that will be acknowledged after this one will cause the sampler
// to exit the app-limited phase.
endOfAppLimitedPhase congestion.PacketNumber
// Record of the connection state at the point where each packet in flight was
// sent, indexed by the packet number.
connectionStats *ConnectionStates
}
func NewBandwidthSampler() *BandwidthSampler {
return &BandwidthSampler{
connectionStats: &ConnectionStates{
stats: make(map[congestion.PacketNumber]*ConnectionStateOnSentPacket),
},
}
}
// OnPacketSent Inputs the sent packet information into the sampler. Assumes that all
// packets are sent in order. The information about the packet will not be
// released from the sampler until it the packet is either acknowledged or
// declared lost.
func (s *BandwidthSampler) OnPacketSent(sentTime time.Time, lastSentPacket congestion.PacketNumber, sentBytes, bytesInFlight congestion.ByteCount, hasRetransmittableData bool) {
s.lastSendPacket = lastSentPacket
if !hasRetransmittableData {
return
}
s.totalBytesSent += sentBytes
// If there are no packets in flight, the time at which the new transmission
// opens can be treated as the A_0 point for the purpose of bandwidth
// sampling. This underestimates bandwidth to some extent, and produces some
// artificially low samples for most packets in flight, but it provides with
// samples at important points where we would not have them otherwise, most
// importantly at the beginning of the connection.
if bytesInFlight == 0 {
s.lastAckedPacketAckTime = sentTime
s.totalBytesSentAtLastAckedPacket = s.totalBytesSent
// In this situation ack compression is not a concern, set send rate to
// effectively infinite.
s.lastAckedPacketSentTime = sentTime
}
s.connectionStats.Insert(lastSentPacket, sentTime, sentBytes, s)
}
// OnPacketAcked Notifies the sampler that the |lastAckedPacket| is acknowledged. Returns a
// bandwidth sample. If no bandwidth sample is available,
// QuicBandwidth::Zero() is returned.
func (s *BandwidthSampler) OnPacketAcked(ackTime time.Time, lastAckedPacket congestion.PacketNumber) *BandwidthSample {
sentPacketState := s.connectionStats.Get(lastAckedPacket)
if sentPacketState == nil {
return NewBandwidthSample()
}
sample := s.onPacketAckedInner(ackTime, lastAckedPacket, sentPacketState)
s.connectionStats.Remove(lastAckedPacket)
return sample
}
// onPacketAckedInner Handles the actual bandwidth calculations, whereas the outer method handles
// retrieving and removing |sentPacket|.
func (s *BandwidthSampler) onPacketAckedInner(ackTime time.Time, lastAckedPacket congestion.PacketNumber, sentPacket *ConnectionStateOnSentPacket) *BandwidthSample {
s.totalBytesAcked += sentPacket.size
s.totalBytesSentAtLastAckedPacket = sentPacket.sendTimeState.totalBytesSent
s.lastAckedPacketSentTime = sentPacket.sendTime
s.lastAckedPacketAckTime = ackTime
// Exit app-limited phase once a packet that was sent while the connection is
// not app-limited is acknowledged.
if s.isAppLimited && lastAckedPacket > s.endOfAppLimitedPhase {
s.isAppLimited = false
}
// There might have been no packets acknowledged at the moment when the
// current packet was sent. In that case, there is no bandwidth sample to
// make.
if sentPacket.lastAckedPacketSentTime.IsZero() {
return NewBandwidthSample()
}
// Infinite rate indicates that the sampler is supposed to discard the
// current send rate sample and use only the ack rate.
sendRate := InfiniteBandwidth
if sentPacket.sendTime.After(sentPacket.lastAckedPacketSentTime) {
sendRate = BandwidthFromDelta(sentPacket.sendTimeState.totalBytesSent-sentPacket.totalBytesSentAtLastAckedPacket, sentPacket.sendTime.Sub(sentPacket.lastAckedPacketSentTime))
}
// During the slope calculation, ensure that ack time of the current packet is
// always larger than the time of the previous packet, otherwise division by
// zero or integer underflow can occur.
if !ackTime.After(sentPacket.lastAckedPacketAckTime) {
// TODO(wub): Compare this code count before and after fixing clock jitter
// issue.
// if sentPacket.lastAckedPacketAckTime.Equal(sentPacket.sendTime) {
// This is the 1st packet after quiescense.
// QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 1, 2);
// } else {
// QUIC_CODE_COUNT_N(quic_prev_ack_time_larger_than_current_ack_time, 2, 2);
// }
return NewBandwidthSample()
}
ackRate := BandwidthFromDelta(s.totalBytesAcked-sentPacket.sendTimeState.totalBytesAcked,
ackTime.Sub(sentPacket.lastAckedPacketAckTime))
// Note: this sample does not account for delayed acknowledgement time. This
// means that the RTT measurements here can be artificially high, especially
// on low bandwidth connections.
sample := &BandwidthSample{
bandwidth: minBandwidth(sendRate, ackRate),
rtt: ackTime.Sub(sentPacket.sendTime),
}
SentPacketToSendTimeState(sentPacket, &sample.stateAtSend)
return sample
}
// OnPacketLost Informs the sampler that a packet is considered lost and it should no
// longer keep track of it.
func (s *BandwidthSampler) OnPacketLost(packetNumber congestion.PacketNumber) SendTimeState {
ok, sentPacket := s.connectionStats.Remove(packetNumber)
sendTimeState := SendTimeState{
isValid: ok,
}
if sentPacket != nil {
s.totalBytesLost += sentPacket.size
SentPacketToSendTimeState(sentPacket, &sendTimeState)
}
return sendTimeState
}
// OnAppLimited Informs the sampler that the connection is currently app-limited, causing
// the sampler to enter the app-limited phase. The phase will expire by
// itself.
func (s *BandwidthSampler) OnAppLimited() {
s.isAppLimited = true
s.endOfAppLimitedPhase = s.lastSendPacket
}
// SentPacketToSendTimeState Copy a subset of the (private) ConnectionStateOnSentPacket to the (public)
// SendTimeState. Always set send_time_state->is_valid to true.
func SentPacketToSendTimeState(sentPacket *ConnectionStateOnSentPacket, sendTimeState *SendTimeState) {
sendTimeState.isAppLimited = sentPacket.sendTimeState.isAppLimited
sendTimeState.totalBytesSent = sentPacket.sendTimeState.totalBytesSent
sendTimeState.totalBytesAcked = sentPacket.sendTimeState.totalBytesAcked
sendTimeState.totalBytesLost = sentPacket.sendTimeState.totalBytesLost
sendTimeState.isValid = true
}
// ConnectionStates Record of the connection state at the point where each packet in flight was
// sent, indexed by the packet number.
// FIXME: using LinkedList replace map to fast remove all the packets lower than the specified packet number.
type ConnectionStates struct {
stats map[congestion.PacketNumber]*ConnectionStateOnSentPacket
}
func (s *ConnectionStates) Insert(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) bool {
if _, ok := s.stats[packetNumber]; ok {
return false
}
s.stats[packetNumber] = NewConnectionStateOnSentPacket(packetNumber, sentTime, bytes, sampler)
return true
}
func (s *ConnectionStates) Get(packetNumber congestion.PacketNumber) *ConnectionStateOnSentPacket {
return s.stats[packetNumber]
}
func (s *ConnectionStates) Remove(packetNumber congestion.PacketNumber) (bool, *ConnectionStateOnSentPacket) {
state, ok := s.stats[packetNumber]
if ok {
delete(s.stats, packetNumber)
}
return ok, state
}
func NewConnectionStateOnSentPacket(packetNumber congestion.PacketNumber, sentTime time.Time, bytes congestion.ByteCount, sampler *BandwidthSampler) *ConnectionStateOnSentPacket {
return &ConnectionStateOnSentPacket{
packetNumber: packetNumber,
sendTime: sentTime,
size: bytes,
lastAckedPacketSentTime: sampler.lastAckedPacketSentTime,
lastAckedPacketAckTime: sampler.lastAckedPacketAckTime,
totalBytesSentAtLastAckedPacket: sampler.totalBytesSentAtLastAckedPacket,
sendTimeState: SendTimeState{
isValid: true,
isAppLimited: sampler.isAppLimited,
totalBytesSent: sampler.totalBytesSent,
totalBytesAcked: sampler.totalBytesAcked,
totalBytesLost: sampler.totalBytesLost,
},
}
}

File diff suppressed because it is too large Load Diff

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