Merge branch 'SagerNet:dev-next' into patch-1

This commit is contained in:
QIN2DIM 2023-08-08 20:42:29 +08:00 committed by GitHub
commit be71354d7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 5460 additions and 407 deletions

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()
}

2
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,6 +47,7 @@ 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))

View File

@ -1,6 +1,9 @@
package build_shared
import "github.com/sagernet/sing/common/shell"
import (
"github.com/sagernet/sing-box/common/badversion"
"github.com/sagernet/sing/common/shell"
)
func ReadTag() (string, error) {
currentTag, err := shell.Exec("git", "describe", "--tags").ReadOutput()
@ -12,5 +15,9 @@ func ReadTag() (string, error) {
return currentTag[1:], nil
}
shortCommit, _ := shell.Exec("git", "rev-parse", "--short", "HEAD").ReadOutput()
return currentTagRev[1:] + "-" + shortCommit, nil
version := badversion.Parse(currentTagRev[1:])
if version.PreReleaseIdentifier == "" {
version.Patch++
}
return version.String() + "-" + shortCommit, nil
}

View File

@ -11,6 +11,7 @@ type Version struct {
Major int
Minor int
Patch int
Commit string
PreReleaseIdentifier string
PreReleaseVersion int
}
@ -37,16 +38,21 @@ func (v Version) After(anotherVersion Version) bool {
return false
}
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
if v.PreReleaseIdentifier == anotherVersion.PreReleaseIdentifier {
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
return true
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
return false
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false
}
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
}
return false
}
@ -95,7 +101,7 @@ func Parse(versionName string) (version Version) {
version.PreReleaseIdentifier = "beta"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
} else {
version.PreReleaseIdentifier = identifier
version.Commit = identifier
}
}
}

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

@ -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

@ -7,7 +7,7 @@ const (
TypeDirect = "direct"
TypeBlock = "block"
TypeDNS = "dns"
TypeSocks = "socks"
TypeSOCKS = "socks"
TypeHTTP = "http"
TypeMixed = "mixed"
TypeShadowsocks = "shadowsocks"
@ -21,9 +21,55 @@ const (
TypeShadowTLS = "shadowtls"
TypeShadowsocksR = "shadowsocksr"
TypeVLESS = "vless"
TypeTUIC = "tuic"
)
const (
TypeSelector = "selector"
TypeURLTest = "urltest"
)
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeDirect:
return "Direct"
case TypeBlock:
return "Block"
case TypeDNS:
return "DNS"
case TypeSOCKS:
return "SOCKS"
case TypeHTTP:
return "HTTP"
case TypeShadowsocks:
return "Shadowsocks"
case TypeVMess:
return "VMess"
case TypeTrojan:
return "Trojan"
case TypeNaive:
return "Naive"
case TypeWireGuard:
return "WireGuard"
case TypeHysteria:
return "Hysteria"
case TypeTor:
return "Tor"
case TypeSSH:
return "SSH"
case TypeShadowTLS:
return "ShadowTLS"
case TypeShadowsocksR:
return "ShadowsocksR"
case TypeVLESS:
return "VLESS"
case TypeTUIC:
return "TUIC"
case TypeSelector:
return "Selector"
case TypeURLTest:
return "URLTest"
default:
return "Unknown"
}
}

View File

@ -1,3 +1,18 @@
#### 1.3.6
* Fixes and improvements
#### 1.3.5
* Fixes and improvements
* Introducing our [Apple tvOS](/installation/clients/sft) client applications **1**
* Add per app proxy and app installed/updated trigger support for Android client
* Add profile sharing support for Android/iOS/macOS clients
**1**:
Due to the requirement of tvOS 17, the app cannot be submitted to the App Store for the time being, and can only be downloaded through TestFlight.
#### 1.3.4
* Fixes and improvements

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

@ -9,6 +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)
#### Note

View File

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

View File

@ -17,12 +17,13 @@ var bucketSelected = []byte("selected")
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) {

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

@ -63,38 +63,10 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
var info badjson.JSONObject
var clashType string
switch detour.Type() {
case C.TypeDirect:
clashType = "Direct"
case C.TypeBlock:
clashType = "Reject"
case C.TypeSocks:
clashType = "Socks"
case C.TypeHTTP:
clashType = "HTTP"
case C.TypeShadowsocks:
clashType = "Shadowsocks"
case C.TypeVMess:
clashType = "VMess"
case C.TypeTrojan:
clashType = "Trojan"
case C.TypeHysteria:
clashType = "Hysteria"
case C.TypeWireGuard:
clashType = "WireGuard"
case C.TypeShadowsocksR:
clashType = "ShadowsocksR"
case C.TypeVLESS:
clashType = "VLESS"
case C.TypeTor:
clashType = "Tor"
case C.TypeSSH:
clashType = "SSH"
case C.TypeSelector:
clashType = "Selector"
case C.TypeURLTest:
clashType = "URLTest"
default:
clashType = "Direct"
clashType = C.ProxyDisplayName(detour.Type())
}
info.Put("type", clashType)
info.Put("name", detour.Tag())

View File

@ -4,6 +4,7 @@ import (
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
@ -71,6 +72,11 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
}
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(2 * time.Second):
}
select {
case <-ctx.Done():
return ctx.Err()
case <-s.urlTestUpdate:

View File

@ -83,7 +83,7 @@ func (s *CommandServer) listenUNIX() error {
Net: "unix",
})
if err != nil {
return E.Cause(err, "listen")
return E.Cause(err, "listen ", sockPath)
}
if sUserID > 0 {
err = os.Chown(sockPath, sUserID, sGroupID)

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

@ -2,7 +2,9 @@ package libbox
import (
"bytes"
"compress/gzip"
"encoding/binary"
"io"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/rw"
@ -170,19 +172,25 @@ type ProfileContent struct {
}
func (c *ProfileContent) Encode() []byte {
var buffer bytes.Buffer
buffer := new(bytes.Buffer)
buffer.WriteByte(MessageTypeProfileContent)
rw.WriteVString(&buffer, c.Name)
binary.Write(&buffer, binary.BigEndian, c.Type)
rw.WriteVString(&buffer, c.Config)
rw.WriteVString(&buffer, c.RemotePath)
binary.Write(&buffer, binary.BigEndian, c.AutoUpdate)
binary.Write(&buffer, binary.BigEndian, c.LastUpdated)
buffer.WriteByte(0)
writer := gzip.NewWriter(buffer)
rw.WriteVString(writer, c.Name)
binary.Write(writer, binary.BigEndian, c.Type)
rw.WriteVString(writer, c.Config)
if c.Type != ProfileTypeLocal {
rw.WriteVString(writer, c.RemotePath)
binary.Write(writer, binary.BigEndian, c.AutoUpdate)
binary.Write(writer, binary.BigEndian, c.LastUpdated)
}
writer.Flush()
writer.Close()
return buffer.Bytes()
}
func DecodeProfileContent(data []byte) (*ProfileContent, error) {
reader := bytes.NewReader(data)
var reader io.Reader = bytes.NewReader(data)
messageType, err := rw.ReadByte(reader)
if err != nil {
return nil, err
@ -190,6 +198,18 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
if messageType != MessageTypeProfileContent {
return nil, E.New("invalid message")
}
version, err := rw.ReadByte(reader)
if err != nil {
return nil, err
}
if version == 0 {
reader, err = gzip.NewReader(reader)
if err != nil {
return nil, E.Cause(err, "unsupported profile")
}
} else {
return nil, E.Cause(err, "unsupported profile")
}
var content ProfileContent
content.Name, err = rw.ReadVString(reader)
if err != nil {
@ -203,17 +223,19 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
if err != nil {
return nil, err
}
content.RemotePath, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &content.AutoUpdate)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &content.LastUpdated)
if err != nil {
return nil, err
if content.Type != ProfileTypeLocal {
content.RemotePath, err = rw.ReadVString(reader)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &content.AutoUpdate)
if err != nil {
return nil, err
}
err = binary.Read(reader, binary.BigEndian, &content.LastUpdated)
if err != nil {
return nil, err
}
}
return &content, nil
}

View File

@ -16,15 +16,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
ctx context.Context
cancel context.CancelFunc
instance *box.Box
pauseManager pause.Manager
}
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
@ -35,6 +38,8 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
ctx, cancel := context.WithCancel(context.Background())
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
sleepManager := pause.NewDefaultManager(ctx)
ctx = pause.ContextWithManager(ctx, sleepManager)
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
@ -45,9 +50,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
return nil, E.Cause(err, "create service")
}
return &BoxService{
ctx: ctx,
cancel: cancel,
instance: instance,
ctx: ctx,
cancel: cancel,
instance: instance,
pauseManager: sleepManager,
}, nil
}
@ -60,6 +66,15 @@ func (s *BoxService) Close() error {
return s.instance.Close()
}
func (s *BoxService) Sleep() {
s.pauseManager.DevicePause()
_ = s.instance.Router().ResetNetwork()
}
func (s *BoxService) Wake() {
s.pauseManager.DeviceWake()
}
var _ platform.Interface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
@ -144,11 +159,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,
}
}

View File

@ -48,3 +48,7 @@ func Version() string {
func FormatBytes(length int64) string {
return humanize.IBytes(uint64(length))
}
func ProxyDisplayType(proxyType string) string {
return C.ProxyDisplayName(proxyType)
}

45
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require (
berty.tech/go-libtor v1.0.385
github.com/Dreamacro/clash v1.17.0
github.com/caddyserver/certmagic v0.19.0
github.com/caddyserver/certmagic v0.19.1
github.com/cretz/bine v0.2.0
github.com/dustin/go-humanize v1.0.1
github.com/fsnotify/fsnotify v1.6.0
@ -13,42 +13,42 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.0.0
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/mholt/acmez v1.2.0
github.com/miekg/dns v1.1.55
github.com/ooni/go-libtor v1.1.8
github.com/oschwald/maxminddb-golang v1.11.0
github.com/oschwald/maxminddb-golang v1.12.0
github.com/pires/go-proxyproto v0.7.0
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-20230615020047-10f05c797c02
github.com/sagernet/quic-go v0.0.0-20230808085251-fb2cdfa273e5
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.2.9
github.com/sagernet/sing-dns v0.1.8
github.com/sagernet/sing-mux v0.1.2
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-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.20230808120247-47ab78d303db
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
go.uber.org/zap v1.24.0
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
golang.org/x/crypto v0.11.0
go.uber.org/zap v1.25.0
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df
golang.org/x/net v0.12.0
golang.org/x/sys v0.10.0
golang.org/x/net v0.14.0
golang.org/x/sys v0.11.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.56.2
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0
)
@ -61,7 +61,8 @@ require (
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c // indirect
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-20210107165309-348f09dbbbc0 // 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
@ -72,14 +73,11 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
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/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
@ -87,13 +85,12 @@ require (
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect

120
go.sum
View File

@ -8,9 +8,9 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/caddyserver/certmagic v0.19.0 h1:HuJ1Yf1H1jAfmBGrSSQN1XRkafnWcpDtyIiyMV6vmpM=
github.com/caddyserver/certmagic v0.19.0/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/caddyserver/certmagic v0.19.1 h1:4jyOYm2DHvQI8YM0sk6qm62Gl5XznHxiMBMWjMTlQkw=
github.com/caddyserver/certmagic v0.19.1/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -33,13 +33,15 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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=
@ -54,8 +56,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df h1:pF1MMIzEJzJ/MyI4bXYXVYyN8CJgoQ2PPKT2z3O/Cl4=
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c h1:P/3mFnHCv1A/ej4m8pF5EB6FUt9qEL2Q9lfrcUNwCYs=
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
@ -77,29 +79,23 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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=
@ -111,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-20230615020047-10f05c797c02 h1:9S+L1n/4hbe1pCLNTZnnddSNseQda8tuSm/+uRy6p8s=
github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02/go.mod h1:rth94YcHJfkC4mG03JTXmv7mJsDc8MOIIqQrCtoaV4U=
github.com/sagernet/quic-go v0.0.0-20230808085251-fb2cdfa273e5 h1:vg1q39d3bsG8mOAueGk+JywLnf+p6Is9imJu1eRrrs4=
github.com/sagernet/quic-go v0.0.0-20230808085251-fb2cdfa273e5/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.9 h1:3wsTz+JG5Wzy65eZnh6AuCrD2QqcRF6Iq6f7ttmJsAo=
github.com/sagernet/sing v0.2.9/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-dns v0.1.8 h1:zTYnxzA7mssg/Lwd70+RFPi8i3djioGnVS7zKwSF6cg=
github.com/sagernet/sing-dns v0.1.8/go.mod h1:lHv8WMl9GKfMV8Wt1AJTtjVTF/h5/owpGY2YutYZF6g=
github.com/sagernet/sing-mux v0.1.2 h1:av2/m6e+Gh+ECTuJZqYCjJz55BNkot0VyRMkREqyF/g=
github.com/sagernet/sing-mux v0.1.2/go.mod h1:r2V8AlOzXaRCHXK7fILCUGzuI2iILweTaG8C5xlpHxo=
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-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-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=
@ -141,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=
@ -151,7 +147,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@ -159,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=
@ -167,58 +164,74 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
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.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
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 v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
@ -226,7 +239,6 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -24,7 +24,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil
case C.TypeDirect:
return NewDirect(ctx, router, logger, options.Tag, options.DirectOptions), nil
case C.TypeSocks:
case C.TypeSOCKS:
return NewSocks(ctx, router, logger, options.Tag, options.SocksOptions), nil
case C.TypeHTTP:
return NewHTTP(ctx, router, logger, options.Tag, options.HTTPOptions)
@ -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,

View File

@ -27,7 +27,7 @@ type Socks struct {
func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) *Socks {
inbound := &Socks{
myInboundAdapter{
protocol: C.TypeSocks,
protocol: C.TypeSOCKS,
network: []string{N.NetworkTCP},
ctx: ctx,
router: router,

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

@ -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
@ -38,7 +39,7 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
v = h.TProxyOptions
case C.TypeDirect:
v = h.DirectOptions
case C.TypeSocks:
case C.TypeSOCKS:
v = h.SocksOptions
case C.TypeHTTP:
v = h.HTTPOptions
@ -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)
}
@ -79,7 +82,7 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
v = &h.TProxyOptions
case C.TypeDirect:
v = &h.DirectOptions
case C.TypeSocks:
case C.TypeSOCKS:
v = &h.SocksOptions
case C.TypeHTTP:
v = &h.HTTPOptions
@ -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:"-"`
}
@ -36,7 +37,7 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
v = h.DirectOptions
case C.TypeBlock, C.TypeDNS:
v = nil
case C.TypeSocks:
case C.TypeSOCKS:
v = h.SocksOptions
case C.TypeHTTP:
v = h.HTTPOptions
@ -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:
@ -81,7 +84,7 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
v = &h.DirectOptions
case C.TypeBlock, C.TypeDNS:
v = nil
case C.TypeSocks:
case C.TypeSOCKS:
v = &h.SocksOptions
case C.TypeHTTP:
v = &h.HTTPOptions
@ -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

@ -27,7 +27,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return NewBlock(logger, tag), nil
case C.TypeDNS:
return NewDNS(router, tag), nil
case C.TypeSocks:
case C.TypeSOCKS:
return NewSocks(router, logger, tag, options.SocksOptions)
case C.TypeHTTP:
return NewHTTP(router, logger, tag, options.HTTPOptions)
@ -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,7 +50,7 @@ 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

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,16 +37,20 @@ 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,
protocol: C.TypeSOCKS,
network: options.Network.Build(),
router: router,
logger: logger,
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

@ -19,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 (
@ -137,21 +138,22 @@ 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 {
ctx context.Context
router adapter.Router
logger log.Logger
outbounds []adapter.Outbound
link string
interval time.Duration
tolerance uint16
history *urltest.HistoryStorage
checking atomic.Bool
ctx context.Context
router adapter.Router
logger log.Logger
outbounds []adapter.Outbound
link string
interval time.Duration
tolerance uint16
history *urltest.HistoryStorage
checking atomic.Bool
pauseManager pause.Manager
access sync.Mutex
ticker *time.Ticker
@ -173,15 +175,16 @@ func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logg
history = urltest.NewHistoryStorage()
}
return &URLTestGroup{
ctx: ctx,
router: router,
logger: logger,
outbounds: outbounds,
link: link,
interval: interval,
tolerance: tolerance,
history: history,
close: make(chan struct{}),
ctx: ctx,
router: router,
logger: logger,
outbounds: outbounds,
link: link,
interval: interval,
tolerance: tolerance,
history: history,
close: make(chan struct{}),
pauseManager: pause.ManagerFromContext(ctx),
}
}
@ -263,6 +266,7 @@ func (g *URLTestGroup) Fallback(used adapter.Outbound) []adapter.Outbound {
func (g *URLTestGroup) loopCheck() {
go g.CheckOutbounds(true)
for {
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
}
@ -970,17 +981,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 +1005,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 +1017,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,13 +10,13 @@ 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.8-0.20230707055657-7c9b4d624da7
github.com/sagernet/sing-shadowsocks v0.2.3-0.20230703131347-b044960bd355
github.com/sagernet/sing-shadowsocks2 v0.1.2-0.20230703131506-ca0c6adde968
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
github.com/stretchr/testify v1.8.4
go.uber.org/goleak v1.2.0
golang.org/x/net v0.12.0
golang.org/x/net v0.14.0
)
require (
@ -26,7 +26,7 @@ require (
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/caddyserver/certmagic v0.18.2 // indirect
github.com/caddyserver/certmagic v0.19.1 // indirect
github.com/cloudflare/circl v1.2.1-0.20221019164342-6ab4dfed8f3c // indirect
github.com/cretz/bine v0.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@ -34,17 +34,17 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-chi/chi/v5 v5.0.8 // indirect
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-chi/cors v1.2.1 // indirect
github.com/go-chi/render v1.0.2 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gogo/protobuf v1.3.2 // 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
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df // indirect
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/klauspost/compress v1.15.15 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
@ -54,11 +54,11 @@ require (
github.com/miekg/dns v1.1.55 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/ooni/go-libtor v1.1.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
@ -66,41 +66,41 @@ require (
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-20230615020047-10f05c797c02 // indirect
github.com/sagernet/quic-go v0.0.0-20230808085251-fb2cdfa273e5 // indirect
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
github.com/sagernet/sing-dns v0.1.7-0.20230703131656-fd65b6178bf9 // indirect
github.com/sagernet/sing-mux v0.1.1-0.20230703132253-2cedde0fbc90 // indirect
github.com/sagernet/sing-shadowtls v0.1.3-0.20230703132509-93bbad3057e4 // indirect
github.com/sagernet/sing-tun v0.1.9-0.20230703134424-fd850d00e5cd // indirect
github.com/sagernet/sing-vmess v0.1.7-0.20230711074224-7d2a9a318162 // 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-shadowtls v0.1.4 // indirect
github.com/sagernet/sing-tun v0.1.12-0.20230808120247-47ab78d303db // 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
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0 // indirect
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 // indirect
golang.org/x/crypto v0.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
go4.org/netipx v0.0.0-20230728184502-ec4c8b891b28 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.56.2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect
google.golang.org/grpc v1.57.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.4.0 // indirect

View File

@ -11,9 +11,9 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/caddyserver/certmagic v0.18.2 h1:Nj2+M+A2Ho9IF6n1wUSbra4mX1X6ALzWpul9HooprHA=
github.com/caddyserver/certmagic v0.18.2/go.mod h1:cLsgYXecH1iVUPjDXw15/1SKjZk/TK+aFfQk5FnugGQ=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/caddyserver/certmagic v0.19.1 h1:4jyOYm2DHvQI8YM0sk6qm62Gl5XznHxiMBMWjMTlQkw=
github.com/caddyserver/certmagic v0.19.1/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@ -37,16 +37,17 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
@ -64,8 +65,8 @@ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLe
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df h1:pF1MMIzEJzJ/MyI4bXYXVYyN8CJgoQ2PPKT2z3O/Cl4=
github.com/insomniacslk/dhcp v0.0.0-20230612134759-b20c9ba983df/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c h1:P/3mFnHCv1A/ej4m8pF5EB6FUt9qEL2Q9lfrcUNwCYs=
github.com/insomniacslk/dhcp v0.0.0-20230731140434-0f9eb93a696c/go.mod h1:7474bZ1YNCvarT6WFKie4kEET6J0KYRDC4XJqqXzQW4=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
@ -73,6 +74,7 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -89,17 +91,17 @@ github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbD
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w=
github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
@ -116,6 +118,7 @@ github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc8
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/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=
@ -124,28 +127,32 @@ 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-20230615020047-10f05c797c02 h1:9S+L1n/4hbe1pCLNTZnnddSNseQda8tuSm/+uRy6p8s=
github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02/go.mod h1:rth94YcHJfkC4mG03JTXmv7mJsDc8MOIIqQrCtoaV4U=
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-20230808085251-fb2cdfa273e5/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.8-0.20230707055657-7c9b4d624da7 h1:sKQu4Gc6vMfzleoFK3h8ROfn/TP2U36qQME3/iYaoqA=
github.com/sagernet/sing v0.2.8-0.20230707055657-7c9b4d624da7/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-dns v0.1.7-0.20230703131656-fd65b6178bf9 h1:35qe74ygIKj5uQkDDD0Xtv+iWOspQsS/Lqhs2XiY0Ak=
github.com/sagernet/sing-dns v0.1.7-0.20230703131656-fd65b6178bf9/go.mod h1:wgmbh0yruJXRO8tmfwPx6hOl6pyReWRoeHdkRehWkmw=
github.com/sagernet/sing-mux v0.1.1-0.20230703132253-2cedde0fbc90 h1:aEe2HrRc9OTS7IZ8RHyh224OhltnwRQs4/y89UsHPo8=
github.com/sagernet/sing-mux v0.1.1-0.20230703132253-2cedde0fbc90/go.mod h1:sm126rB5EUi9HLf4jCSHTqo+XRPbh4BoEVeLbr2WRbE=
github.com/sagernet/sing-shadowsocks v0.2.3-0.20230703131347-b044960bd355 h1:XOgNZYnkDrx5qtNS4kqIOHMhjZuc7mJ2pY/x3EyZX8Q=
github.com/sagernet/sing-shadowsocks v0.2.3-0.20230703131347-b044960bd355/go.mod h1:atEATsxqPo8qCPcFt8Rw7TFEJ70egCoMR7PziX4jmjI=
github.com/sagernet/sing-shadowsocks2 v0.1.2-0.20230703131506-ca0c6adde968 h1:UctXygnZfqsFR+2hZXfpWK3pSYKLbBQMuli9GDE6QU0=
github.com/sagernet/sing-shadowsocks2 v0.1.2-0.20230703131506-ca0c6adde968/go.mod h1:xFxUGbtnqRLxtQftCILFeKf43GE6S83f0I6CsO9BxGE=
github.com/sagernet/sing-shadowtls v0.1.3-0.20230703132509-93bbad3057e4 h1:ZjLyCkEENqXzGp4PRZbQGk5wPzEq0Rg+/2jK82lmy3Q=
github.com/sagernet/sing-shadowtls v0.1.3-0.20230703132509-93bbad3057e4/go.mod h1:8ZSSHJSNOG7cUCUYJemZNH873EsKdFqABykTypoS/2M=
github.com/sagernet/sing-tun v0.1.9-0.20230703134424-fd850d00e5cd h1:lkJA/P1L2XE5lNDnzA2fygx6DZIks3Sx87GN2OE0jNY=
github.com/sagernet/sing-tun v0.1.9-0.20230703134424-fd850d00e5cd/go.mod h1:XNQoXtvsmeva+dADmo/57KktLNgm5ubOyR67Niahqj8=
github.com/sagernet/sing-vmess v0.1.7-0.20230711074224-7d2a9a318162 h1:EvEbqAdZJalFqLwKSHIixH+TRed0iY2f8KatI7mtt1Q=
github.com/sagernet/sing-vmess v0.1.7-0.20230711074224-7d2a9a318162/go.mod h1:FHEBboQRvfBDbZd/fAKBpgR4w7QbYyzNL/MLd22/iTk=
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-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.12-0.20230807123152-0a68b9f1d873 h1:f1ejTKI6R+rQ2vHyD5pNHy0V+MhfBD1l8wiYyhTscnI=
github.com/sagernet/sing-tun v0.1.12-0.20230807123152-0a68b9f1d873/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230807123232-b4cea12022c0/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230808064040-326c20ab22af/go.mod h1:XsyIVKd/Qp+2SdLZWGbavHtcpE7J7XU3S1zJmcoj9Ck=
github.com/sagernet/sing-tun v0.1.12-0.20230808120247-47ab78d303db/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=
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0=
github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 h1:2ItpW1nMNkPzmBTxV0/eClCklHrFSQMnUGcpUmJxVeE=
@ -156,6 +163,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=
@ -164,7 +172,7 @@ github.com/spyzhov/ajson v0.7.1 h1:1MDIlPc6x0zjNtpa7tDzRAyFAvRX+X8ZsvtYz5lZg6A=
github.com/spyzhov/ajson v0.7.1/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@ -174,25 +182,29 @@ 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/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=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
@ -206,8 +218,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
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-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
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=
@ -227,15 +239,15 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
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=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
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.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
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=
@ -249,17 +261,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
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=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

@ -491,7 +491,7 @@ func testVLESSXrayInbound(t *testing.T, flow string) {
},
},
{
Type: C.TypeSocks,
Type: C.TypeSOCKS,
Tag: "vless-out",
SocksOptions: option.SocksOutboundOptions{
ServerOptions: option.ServerOptions{

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
}

View File

@ -50,8 +50,8 @@ func (b *BrutalSender) TimeUntilSend(bytesInFlight congestion.ByteCount) time.Ti
return b.pacer.TimeUntilSend()
}
func (b *BrutalSender) HasPacingBudget() bool {
return b.pacer.Budget(time.Now()) >= b.maxDatagramSize
func (b *BrutalSender) HasPacingBudget(now time.Time) bool {
return b.pacer.Budget(now) >= b.maxDatagramSize
}
func (b *BrutalSender) CanSend(bytesInFlight congestion.ByteCount) bool {

View File

@ -6,6 +6,7 @@ import (
"encoding/hex"
"net"
"os"
"sync"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
@ -84,6 +85,7 @@ func (c *ClientConn) Upstream() any {
type ClientPacketConn struct {
net.Conn
access sync.Mutex
key [KeyLength]byte
headerWritten bool
}
@ -105,9 +107,15 @@ func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
if !c.headerWritten {
err := ClientHandshakePacket(c.Conn, c.key, destination, buffer)
c.headerWritten = true
return err
c.access.Lock()
if c.headerWritten {
c.access.Unlock()
} else {
err := ClientHandshakePacket(c.Conn, c.key, destination, buffer)
c.headerWritten = true
c.access.Unlock()
return err
}
}
return WritePacket(c.Conn, buffer, destination)
}

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),
)

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

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

View File

@ -0,0 +1,20 @@
package congestion
import "time"
// A Clock returns the current time
type Clock interface {
Now() time.Time
}
// DefaultClock implements the Clock interface using the Go stdlib clock.
type DefaultClock struct {
TimeFunc func() time.Time
}
var _ Clock = DefaultClock{}
// Now gets the current time
func (c DefaultClock) Now() time.Time {
return c.TimeFunc()
}

View File

@ -0,0 +1,213 @@
package congestion
import (
"math"
"time"
"github.com/sagernet/quic-go/congestion"
)
// This cubic implementation is based on the one found in Chromiums's QUIC
// implementation, in the files net/quic/congestion_control/cubic.{hh,cc}.
// Constants based on TCP defaults.
// The following constants are in 2^10 fractions of a second instead of ms to
// allow a 10 shift right to divide.
// 1024*1024^3 (first 1024 is from 0.100^3)
// where 0.100 is 100 ms which is the scaling round trip time.
const (
cubeScale = 40
cubeCongestionWindowScale = 410
cubeFactor congestion.ByteCount = 1 << cubeScale / cubeCongestionWindowScale / maxDatagramSize
// TODO: when re-enabling cubic, make sure to use the actual packet size here
maxDatagramSize = congestion.ByteCount(InitialPacketSizeIPv4)
)
const defaultNumConnections = 1
// Default Cubic backoff factor
const beta float32 = 0.7
// Additional backoff factor when loss occurs in the concave part of the Cubic
// curve. This additional backoff factor is expected to give up bandwidth to
// new concurrent flows and speed up convergence.
const betaLastMax float32 = 0.85
// Cubic implements the cubic algorithm from TCP
type Cubic struct {
clock Clock
// Number of connections to simulate.
numConnections int
// Time when this cycle started, after last loss event.
epoch time.Time
// Max congestion window used just before last loss event.
// Note: to improve fairness to other streams an additional back off is
// applied to this value if the new value is below our latest value.
lastMaxCongestionWindow congestion.ByteCount
// Number of acked bytes since the cycle started (epoch).
ackedBytesCount congestion.ByteCount
// TCP Reno equivalent congestion window in packets.
estimatedTCPcongestionWindow congestion.ByteCount
// Origin point of cubic function.
originPointCongestionWindow congestion.ByteCount
// Time to origin point of cubic function in 2^10 fractions of a second.
timeToOriginPoint uint32
// Last congestion window in packets computed by cubic function.
lastTargetCongestionWindow congestion.ByteCount
}
// NewCubic returns a new Cubic instance
func NewCubic(clock Clock) *Cubic {
c := &Cubic{
clock: clock,
numConnections: defaultNumConnections,
}
c.Reset()
return c
}
// Reset is called after a timeout to reset the cubic state
func (c *Cubic) Reset() {
c.epoch = time.Time{}
c.lastMaxCongestionWindow = 0
c.ackedBytesCount = 0
c.estimatedTCPcongestionWindow = 0
c.originPointCongestionWindow = 0
c.timeToOriginPoint = 0
c.lastTargetCongestionWindow = 0
}
func (c *Cubic) alpha() float32 {
// TCPFriendly alpha is described in Section 3.3 of the CUBIC paper. Note that
// beta here is a cwnd multiplier, and is equal to 1-beta from the paper.
// We derive the equivalent alpha for an N-connection emulation as:
b := c.beta()
return 3 * float32(c.numConnections) * float32(c.numConnections) * (1 - b) / (1 + b)
}
func (c *Cubic) beta() float32 {
// kNConnectionBeta is the backoff factor after loss for our N-connection
// emulation, which emulates the effective backoff of an ensemble of N
// TCP-Reno connections on a single loss event. The effective multiplier is
// computed as:
return (float32(c.numConnections) - 1 + beta) / float32(c.numConnections)
}
func (c *Cubic) betaLastMax() float32 {
// betaLastMax is the additional backoff factor after loss for our
// N-connection emulation, which emulates the additional backoff of
// an ensemble of N TCP-Reno connections on a single loss event. The
// effective multiplier is computed as:
return (float32(c.numConnections) - 1 + betaLastMax) / float32(c.numConnections)
}
// OnApplicationLimited is called on ack arrival when sender is unable to use
// the available congestion window. Resets Cubic state during quiescence.
func (c *Cubic) OnApplicationLimited() {
// When sender is not using the available congestion window, the window does
// not grow. But to be RTT-independent, Cubic assumes that the sender has been
// using the entire window during the time since the beginning of the current
// "epoch" (the end of the last loss recovery period). Since
// application-limited periods break this assumption, we reset the epoch when
// in such a period. This reset effectively freezes congestion window growth
// through application-limited periods and allows Cubic growth to continue
// when the entire window is being used.
c.epoch = time.Time{}
}
// CongestionWindowAfterPacketLoss computes a new congestion window to use after
// a loss event. Returns the new congestion window in packets. The new
// congestion window is a multiplicative decrease of our current window.
func (c *Cubic) CongestionWindowAfterPacketLoss(currentCongestionWindow congestion.ByteCount) congestion.ByteCount {
if currentCongestionWindow+maxDatagramSize < c.lastMaxCongestionWindow {
// We never reached the old max, so assume we are competing with another
// flow. Use our extra back off factor to allow the other flow to go up.
c.lastMaxCongestionWindow = congestion.ByteCount(c.betaLastMax() * float32(currentCongestionWindow))
} else {
c.lastMaxCongestionWindow = currentCongestionWindow
}
c.epoch = time.Time{} // Reset time.
return congestion.ByteCount(float32(currentCongestionWindow) * c.beta())
}
// CongestionWindowAfterAck computes a new congestion window to use after a received ACK.
// Returns the new congestion window in packets. The new congestion window
// follows a cubic function that depends on the time passed since last
// packet loss.
func (c *Cubic) CongestionWindowAfterAck(
ackedBytes congestion.ByteCount,
currentCongestionWindow congestion.ByteCount,
delayMin time.Duration,
eventTime time.Time,
) congestion.ByteCount {
c.ackedBytesCount += ackedBytes
if c.epoch.IsZero() {
// First ACK after a loss event.
c.epoch = eventTime // Start of epoch.
c.ackedBytesCount = ackedBytes // Reset count.
// Reset estimated_tcp_congestion_window_ to be in sync with cubic.
c.estimatedTCPcongestionWindow = currentCongestionWindow
if c.lastMaxCongestionWindow <= currentCongestionWindow {
c.timeToOriginPoint = 0
c.originPointCongestionWindow = currentCongestionWindow
} else {
c.timeToOriginPoint = uint32(math.Cbrt(float64(cubeFactor * (c.lastMaxCongestionWindow - currentCongestionWindow))))
c.originPointCongestionWindow = c.lastMaxCongestionWindow
}
}
// Change the time unit from microseconds to 2^10 fractions per second. Take
// the round trip time in account. This is done to allow us to use shift as a
// divide operator.
elapsedTime := int64(eventTime.Add(delayMin).Sub(c.epoch)/time.Microsecond) << 10 / (1000 * 1000)
// Right-shifts of negative, signed numbers have implementation-dependent
// behavior, so force the offset to be positive, as is done in the kernel.
offset := int64(c.timeToOriginPoint) - elapsedTime
if offset < 0 {
offset = -offset
}
deltaCongestionWindow := congestion.ByteCount(cubeCongestionWindowScale*offset*offset*offset) * maxDatagramSize >> cubeScale
var targetCongestionWindow congestion.ByteCount
if elapsedTime > int64(c.timeToOriginPoint) {
targetCongestionWindow = c.originPointCongestionWindow + deltaCongestionWindow
} else {
targetCongestionWindow = c.originPointCongestionWindow - deltaCongestionWindow
}
// Limit the CWND increase to half the acked bytes.
targetCongestionWindow = Min(targetCongestionWindow, currentCongestionWindow+c.ackedBytesCount/2)
// Increase the window by approximately Alpha * 1 MSS of bytes every
// time we ack an estimated tcp window of bytes. For small
// congestion windows (less than 25), the formula below will
// increase slightly slower than linearly per estimated tcp window
// of bytes.
c.estimatedTCPcongestionWindow += congestion.ByteCount(float32(c.ackedBytesCount) * c.alpha() * float32(maxDatagramSize) / float32(c.estimatedTCPcongestionWindow))
c.ackedBytesCount = 0
// We have a new cubic congestion window.
c.lastTargetCongestionWindow = targetCongestionWindow
// Compute target congestion_window based on cubic target and estimated TCP
// congestion_window, use highest (fastest).
if targetCongestionWindow < c.estimatedTCPcongestionWindow {
targetCongestionWindow = c.estimatedTCPcongestionWindow
}
return targetCongestionWindow
}
// SetNumConnections sets the number of emulated connections
func (c *Cubic) SetNumConnections(n int) {
c.numConnections = n
}

View File

@ -0,0 +1,318 @@
package congestion
import (
"fmt"
"time"
"github.com/sagernet/quic-go/congestion"
"github.com/sagernet/quic-go/logging"
)
const (
maxBurstPackets = 3
renoBeta = 0.7 // Reno backoff factor.
minCongestionWindowPackets = 2
initialCongestionWindow = 32
)
const (
InvalidPacketNumber congestion.PacketNumber = -1
MaxCongestionWindowPackets = 20000
MaxByteCount = congestion.ByteCount(1<<62 - 1)
)
type cubicSender struct {
hybridSlowStart HybridSlowStart
rttStats congestion.RTTStatsProvider
cubic *Cubic
pacer *pacer
clock Clock
reno bool
// Track the largest packet that has been sent.
largestSentPacketNumber congestion.PacketNumber
// Track the largest packet that has been acked.
largestAckedPacketNumber congestion.PacketNumber
// Track the largest packet number outstanding when a CWND cutback occurs.
largestSentAtLastCutback congestion.PacketNumber
// Whether the last loss event caused us to exit slowstart.
// Used for stats collection of slowstartPacketsLost
lastCutbackExitedSlowstart bool
// Congestion window in bytes.
congestionWindow congestion.ByteCount
// Slow start congestion window in bytes, aka ssthresh.
slowStartThreshold congestion.ByteCount
// ACK counter for the Reno implementation.
numAckedPackets uint64
initialCongestionWindow congestion.ByteCount
initialMaxCongestionWindow congestion.ByteCount
maxDatagramSize congestion.ByteCount
lastState logging.CongestionState
tracer logging.ConnectionTracer
}
var _ congestion.CongestionControl = &cubicSender{}
// NewCubicSender makes a new cubic sender
func NewCubicSender(
clock Clock,
initialMaxDatagramSize congestion.ByteCount,
reno bool,
tracer logging.ConnectionTracer,
) *cubicSender {
return newCubicSender(
clock,
reno,
initialMaxDatagramSize,
initialCongestionWindow*initialMaxDatagramSize,
MaxCongestionWindowPackets*initialMaxDatagramSize,
tracer,
)
}
func newCubicSender(
clock Clock,
reno bool,
initialMaxDatagramSize,
initialCongestionWindow,
initialMaxCongestionWindow congestion.ByteCount,
tracer logging.ConnectionTracer,
) *cubicSender {
c := &cubicSender{
largestSentPacketNumber: InvalidPacketNumber,
largestAckedPacketNumber: InvalidPacketNumber,
largestSentAtLastCutback: InvalidPacketNumber,
initialCongestionWindow: initialCongestionWindow,
initialMaxCongestionWindow: initialMaxCongestionWindow,
congestionWindow: initialCongestionWindow,
slowStartThreshold: MaxByteCount,
cubic: NewCubic(clock),
clock: clock,
reno: reno,
tracer: tracer,
maxDatagramSize: initialMaxDatagramSize,
}
c.pacer = newPacer(c.BandwidthEstimate)
if c.tracer != nil {
c.lastState = logging.CongestionStateSlowStart
c.tracer.UpdatedCongestionState(logging.CongestionStateSlowStart)
}
return c
}
func (c *cubicSender) SetRTTStatsProvider(provider congestion.RTTStatsProvider) {
c.rttStats = provider
}
// TimeUntilSend returns when the next packet should be sent.
func (c *cubicSender) TimeUntilSend(_ congestion.ByteCount) time.Time {
return c.pacer.TimeUntilSend()
}
func (c *cubicSender) HasPacingBudget(now time.Time) bool {
return c.pacer.Budget(now) >= c.maxDatagramSize
}
func (c *cubicSender) maxCongestionWindow() congestion.ByteCount {
return c.maxDatagramSize * MaxCongestionWindowPackets
}
func (c *cubicSender) minCongestionWindow() congestion.ByteCount {
return c.maxDatagramSize * minCongestionWindowPackets
}
func (c *cubicSender) OnPacketSent(
sentTime time.Time,
_ congestion.ByteCount,
packetNumber congestion.PacketNumber,
bytes congestion.ByteCount,
isRetransmittable bool,
) {
c.pacer.SentPacket(sentTime, bytes)
if !isRetransmittable {
return
}
c.largestSentPacketNumber = packetNumber
c.hybridSlowStart.OnPacketSent(packetNumber)
}
func (c *cubicSender) CanSend(bytesInFlight congestion.ByteCount) bool {
return bytesInFlight < c.GetCongestionWindow()
}
func (c *cubicSender) InRecovery() bool {
return c.largestAckedPacketNumber != InvalidPacketNumber && c.largestAckedPacketNumber <= c.largestSentAtLastCutback
}
func (c *cubicSender) InSlowStart() bool {
return c.GetCongestionWindow() < c.slowStartThreshold
}
func (c *cubicSender) GetCongestionWindow() congestion.ByteCount {
return c.congestionWindow
}
func (c *cubicSender) MaybeExitSlowStart() {
if c.InSlowStart() &&
c.hybridSlowStart.ShouldExitSlowStart(c.rttStats.LatestRTT(), c.rttStats.MinRTT(), c.GetCongestionWindow()/c.maxDatagramSize) {
// exit slow start
c.slowStartThreshold = c.congestionWindow
c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance)
}
}
func (c *cubicSender) OnPacketAcked(
ackedPacketNumber congestion.PacketNumber,
ackedBytes congestion.ByteCount,
priorInFlight congestion.ByteCount,
eventTime time.Time,
) {
c.largestAckedPacketNumber = Max(ackedPacketNumber, c.largestAckedPacketNumber)
if c.InRecovery() {
return
}
c.maybeIncreaseCwnd(ackedPacketNumber, ackedBytes, priorInFlight, eventTime)
if c.InSlowStart() {
c.hybridSlowStart.OnPacketAcked(ackedPacketNumber)
}
}
func (c *cubicSender) OnPacketLost(packetNumber congestion.PacketNumber, lostBytes, priorInFlight congestion.ByteCount) {
// TCP NewReno (RFC6582) says that once a loss occurs, any losses in packets
// already sent should be treated as a single loss event, since it's expected.
if packetNumber <= c.largestSentAtLastCutback {
return
}
c.lastCutbackExitedSlowstart = c.InSlowStart()
c.maybeTraceStateChange(logging.CongestionStateRecovery)
if c.reno {
c.congestionWindow = congestion.ByteCount(float64(c.congestionWindow) * renoBeta)
} else {
c.congestionWindow = c.cubic.CongestionWindowAfterPacketLoss(c.congestionWindow)
}
if minCwnd := c.minCongestionWindow(); c.congestionWindow < minCwnd {
c.congestionWindow = minCwnd
}
c.slowStartThreshold = c.congestionWindow
c.largestSentAtLastCutback = c.largestSentPacketNumber
// reset packet count from congestion avoidance mode. We start
// counting again when we're out of recovery.
c.numAckedPackets = 0
}
// Called when we receive an ack. Normal TCP tracks how many packets one ack
// represents, but quic has a separate ack for each packet.
func (c *cubicSender) maybeIncreaseCwnd(
_ congestion.PacketNumber,
ackedBytes congestion.ByteCount,
priorInFlight congestion.ByteCount,
eventTime time.Time,
) {
// Do not increase the congestion window unless the sender is close to using
// the current window.
if !c.isCwndLimited(priorInFlight) {
c.cubic.OnApplicationLimited()
c.maybeTraceStateChange(logging.CongestionStateApplicationLimited)
return
}
if c.congestionWindow >= c.maxCongestionWindow() {
return
}
if c.InSlowStart() {
// TCP slow start, exponential growth, increase by one for each ACK.
c.congestionWindow += c.maxDatagramSize
c.maybeTraceStateChange(logging.CongestionStateSlowStart)
return
}
// Congestion avoidance
c.maybeTraceStateChange(logging.CongestionStateCongestionAvoidance)
if c.reno {
// Classic Reno congestion avoidance.
c.numAckedPackets++
if c.numAckedPackets >= uint64(c.congestionWindow/c.maxDatagramSize) {
c.congestionWindow += c.maxDatagramSize
c.numAckedPackets = 0
}
} else {
c.congestionWindow = Min(c.maxCongestionWindow(), c.cubic.CongestionWindowAfterAck(ackedBytes, c.congestionWindow, c.rttStats.MinRTT(), eventTime))
}
}
func (c *cubicSender) isCwndLimited(bytesInFlight congestion.ByteCount) bool {
congestionWindow := c.GetCongestionWindow()
if bytesInFlight >= congestionWindow {
return true
}
availableBytes := congestionWindow - bytesInFlight
slowStartLimited := c.InSlowStart() && bytesInFlight > congestionWindow/2
return slowStartLimited || availableBytes <= maxBurstPackets*c.maxDatagramSize
}
// BandwidthEstimate returns the current bandwidth estimate
func (c *cubicSender) BandwidthEstimate() Bandwidth {
if c.rttStats == nil {
return infBandwidth
}
srtt := c.rttStats.SmoothedRTT()
if srtt == 0 {
// If we haven't measured an rtt, the bandwidth estimate is unknown.
return infBandwidth
}
return BandwidthFromDelta(c.GetCongestionWindow(), srtt)
}
// OnRetransmissionTimeout is called on an retransmission timeout
func (c *cubicSender) OnRetransmissionTimeout(packetsRetransmitted bool) {
c.largestSentAtLastCutback = InvalidPacketNumber
if !packetsRetransmitted {
return
}
c.hybridSlowStart.Restart()
c.cubic.Reset()
c.slowStartThreshold = c.congestionWindow / 2
c.congestionWindow = c.minCongestionWindow()
}
// OnConnectionMigration is called when the connection is migrated (?)
func (c *cubicSender) OnConnectionMigration() {
c.hybridSlowStart.Restart()
c.largestSentPacketNumber = InvalidPacketNumber
c.largestAckedPacketNumber = InvalidPacketNumber
c.largestSentAtLastCutback = InvalidPacketNumber
c.lastCutbackExitedSlowstart = false
c.cubic.Reset()
c.numAckedPackets = 0
c.congestionWindow = c.initialCongestionWindow
c.slowStartThreshold = c.initialMaxCongestionWindow
}
func (c *cubicSender) maybeTraceStateChange(new logging.CongestionState) {
if c.tracer == nil || new == c.lastState {
return
}
c.tracer.UpdatedCongestionState(new)
c.lastState = new
}
func (c *cubicSender) SetMaxDatagramSize(s congestion.ByteCount) {
if s < c.maxDatagramSize {
panic(fmt.Sprintf("congestion BUG: decreased max datagram size from %d to %d", c.maxDatagramSize, s))
}
cwndIsMinCwnd := c.congestionWindow == c.minCongestionWindow()
c.maxDatagramSize = s
if cwndIsMinCwnd {
c.congestionWindow = c.minCongestionWindow()
}
c.pacer.SetMaxDatagramSize(s)
}

View File

@ -0,0 +1,112 @@
package congestion
import (
"time"
"github.com/sagernet/quic-go/congestion"
)
// Note(pwestin): the magic clamping numbers come from the original code in
// tcp_cubic.c.
const hybridStartLowWindow = congestion.ByteCount(16)
// Number of delay samples for detecting the increase of delay.
const hybridStartMinSamples = uint32(8)
// Exit slow start if the min rtt has increased by more than 1/8th.
const hybridStartDelayFactorExp = 3 // 2^3 = 8
// The original paper specifies 2 and 8ms, but those have changed over time.
const (
hybridStartDelayMinThresholdUs = int64(4000)
hybridStartDelayMaxThresholdUs = int64(16000)
)
// HybridSlowStart implements the TCP hybrid slow start algorithm
type HybridSlowStart struct {
endPacketNumber congestion.PacketNumber
lastSentPacketNumber congestion.PacketNumber
started bool
currentMinRTT time.Duration
rttSampleCount uint32
hystartFound bool
}
// StartReceiveRound is called for the start of each receive round (burst) in the slow start phase.
func (s *HybridSlowStart) StartReceiveRound(lastSent congestion.PacketNumber) {
s.endPacketNumber = lastSent
s.currentMinRTT = 0
s.rttSampleCount = 0
s.started = true
}
// IsEndOfRound returns true if this ack is the last packet number of our current slow start round.
func (s *HybridSlowStart) IsEndOfRound(ack congestion.PacketNumber) bool {
return s.endPacketNumber < ack
}
// ShouldExitSlowStart should be called on every new ack frame, since a new
// RTT measurement can be made then.
// rtt: the RTT for this ack packet.
// minRTT: is the lowest delay (RTT) we have seen during the session.
// congestionWindow: the congestion window in packets.
func (s *HybridSlowStart) ShouldExitSlowStart(latestRTT time.Duration, minRTT time.Duration, congestionWindow congestion.ByteCount) bool {
if !s.started {
// Time to start the hybrid slow start.
s.StartReceiveRound(s.lastSentPacketNumber)
}
if s.hystartFound {
return true
}
// Second detection parameter - delay increase detection.
// Compare the minimum delay (s.currentMinRTT) of the current
// burst of packets relative to the minimum delay during the session.
// Note: we only look at the first few(8) packets in each burst, since we
// only want to compare the lowest RTT of the burst relative to previous
// bursts.
s.rttSampleCount++
if s.rttSampleCount <= hybridStartMinSamples {
if s.currentMinRTT == 0 || s.currentMinRTT > latestRTT {
s.currentMinRTT = latestRTT
}
}
// We only need to check this once per round.
if s.rttSampleCount == hybridStartMinSamples {
// Divide minRTT by 8 to get a rtt increase threshold for exiting.
minRTTincreaseThresholdUs := int64(minRTT / time.Microsecond >> hybridStartDelayFactorExp)
// Ensure the rtt threshold is never less than 2ms or more than 16ms.
minRTTincreaseThresholdUs = Min(minRTTincreaseThresholdUs, hybridStartDelayMaxThresholdUs)
minRTTincreaseThreshold := time.Duration(Max(minRTTincreaseThresholdUs, hybridStartDelayMinThresholdUs)) * time.Microsecond
if s.currentMinRTT > (minRTT + minRTTincreaseThreshold) {
s.hystartFound = true
}
}
// Exit from slow start if the cwnd is greater than 16 and
// increasing delay is found.
return congestionWindow >= hybridStartLowWindow && s.hystartFound
}
// OnPacketSent is called when a packet was sent
func (s *HybridSlowStart) OnPacketSent(packetNumber congestion.PacketNumber) {
s.lastSentPacketNumber = packetNumber
}
// OnPacketAcked gets invoked after ShouldExitSlowStart, so it's best to end
// the round when the final packet of the burst is received and start it on
// the next incoming ack.
func (s *HybridSlowStart) OnPacketAcked(ackedPacketNumber congestion.PacketNumber) {
if s.IsEndOfRound(ackedPacketNumber) {
s.started = false
}
}
// Started returns true if started
func (s *HybridSlowStart) Started() bool {
return s.started
}
// Restart the slow start phase
func (s *HybridSlowStart) Restart() {
s.started = false
s.hystartFound = false
}

View File

@ -0,0 +1,72 @@
package congestion
import (
"math"
"time"
"golang.org/x/exp/constraints"
)
// InfDuration is a duration of infinite length
const InfDuration = time.Duration(math.MaxInt64)
func Max[T constraints.Ordered](a, b T) T {
if a < b {
return b
}
return a
}
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// MinNonZeroDuration return the minimum duration that's not zero.
func MinNonZeroDuration(a, b time.Duration) time.Duration {
if a == 0 {
return b
}
if b == 0 {
return a
}
return Min(a, b)
}
// AbsDuration returns the absolute value of a time duration
func AbsDuration(d time.Duration) time.Duration {
if d >= 0 {
return d
}
return -d
}
// MinTime returns the earlier time
func MinTime(a, b time.Time) time.Time {
if a.After(b) {
return b
}
return a
}
// MinNonZeroTime returns the earlist time that is not time.Time{}
// If both a and b are time.Time{}, it returns time.Time{}
func MinNonZeroTime(a, b time.Time) time.Time {
if a.IsZero() {
return b
}
if b.IsZero() {
return a
}
return MinTime(a, b)
}
// MaxTime returns the later time
func MaxTime(a, b time.Time) time.Time {
if a.After(b) {
return a
}
return b
}

View File

@ -0,0 +1,81 @@
package congestion
import (
"math"
"time"
"github.com/sagernet/quic-go/congestion"
)
const (
initialMaxDatagramSize = congestion.ByteCount(1252)
MinPacingDelay = time.Millisecond
TimerGranularity = time.Millisecond
maxBurstSizePackets = 10
)
// The pacer implements a token bucket pacing algorithm.
type pacer struct {
budgetAtLastSent congestion.ByteCount
maxDatagramSize congestion.ByteCount
lastSentTime time.Time
getAdjustedBandwidth func() uint64 // in bytes/s
}
func newPacer(getBandwidth func() Bandwidth) *pacer {
p := &pacer{
maxDatagramSize: initialMaxDatagramSize,
getAdjustedBandwidth: func() uint64 {
// Bandwidth is in bits/s. We need the value in bytes/s.
bw := uint64(getBandwidth() / BytesPerSecond)
// Use a slightly higher value than the actual measured bandwidth.
// RTT variations then won't result in under-utilization of the congestion window.
// Ultimately, this will result in sending packets as acknowledgments are received rather than when timers fire,
// provided the congestion window is fully utilized and acknowledgments arrive at regular intervals.
return bw * 5 / 4
},
}
p.budgetAtLastSent = p.maxBurstSize()
return p
}
func (p *pacer) SentPacket(sendTime time.Time, size congestion.ByteCount) {
budget := p.Budget(sendTime)
if size > budget {
p.budgetAtLastSent = 0
} else {
p.budgetAtLastSent = budget - size
}
p.lastSentTime = sendTime
}
func (p *pacer) Budget(now time.Time) congestion.ByteCount {
if p.lastSentTime.IsZero() {
return p.maxBurstSize()
}
budget := p.budgetAtLastSent + (congestion.ByteCount(p.getAdjustedBandwidth())*congestion.ByteCount(now.Sub(p.lastSentTime).Nanoseconds()))/1e9
return Min(p.maxBurstSize(), budget)
}
func (p *pacer) maxBurstSize() congestion.ByteCount {
return Max(
congestion.ByteCount(uint64((MinPacingDelay+TimerGranularity).Nanoseconds())*p.getAdjustedBandwidth())/1e9,
maxBurstSizePackets*p.maxDatagramSize,
)
}
// TimeUntilSend returns when the next packet should be sent.
// It returns the zero value of time.Time if a packet can be sent immediately.
func (p *pacer) TimeUntilSend() time.Time {
if p.budgetAtLastSent >= p.maxDatagramSize {
return time.Time{}
}
return p.lastSentTime.Add(Max(
MinPacingDelay,
time.Duration(math.Ceil(float64(p.maxDatagramSize-p.budgetAtLastSent)*1e9/float64(p.getAdjustedBandwidth())))*time.Nanosecond,
))
}
func (p *pacer) SetMaxDatagramSize(s congestion.ByteCount) {
p.maxDatagramSize = s
}

View File

@ -0,0 +1,132 @@
package congestion
// WindowedFilter Use the following to construct a windowed filter object of type T.
// For example, a min filter using QuicTime as the time type:
//
// WindowedFilter<T, MinFilter<T>, QuicTime, QuicTime::Delta> ObjectName;
//
// A max filter using 64-bit integers as the time type:
//
// WindowedFilter<T, MaxFilter<T>, uint64_t, int64_t> ObjectName;
//
// Specifically, this template takes four arguments:
// 1. T -- type of the measurement that is being filtered.
// 2. Compare -- MinFilter<T> or MaxFilter<T>, depending on the type of filter
// desired.
// 3. TimeT -- the type used to represent timestamps.
// 4. TimeDeltaT -- the type used to represent continuous time intervals between
// two timestamps. Has to be the type of (a - b) if both |a| and |b| are
// of type TimeT.
type WindowedFilter struct {
// Time length of window.
windowLength int64
estimates []Sample
comparator func(int64, int64) bool
}
type Sample struct {
sample int64
time int64
}
// Compares two values and returns true if the first is greater than or equal
// to the second.
func MaxFilter(a, b int64) bool {
return a >= b
}
// Compares two values and returns true if the first is less than or equal
// to the second.
func MinFilter(a, b int64) bool {
return a <= b
}
func NewWindowedFilter(windowLength int64, comparator func(int64, int64) bool) *WindowedFilter {
return &WindowedFilter{
windowLength: windowLength,
estimates: make([]Sample, 3),
comparator: comparator,
}
}
// Changes the window length. Does not update any current samples.
func (f *WindowedFilter) SetWindowLength(windowLength int64) {
f.windowLength = windowLength
}
func (f *WindowedFilter) GetBest() int64 {
return f.estimates[0].sample
}
func (f *WindowedFilter) GetSecondBest() int64 {
return f.estimates[1].sample
}
func (f *WindowedFilter) GetThirdBest() int64 {
return f.estimates[2].sample
}
func (f *WindowedFilter) Update(sample int64, time int64) {
if f.estimates[0].time == 0 || f.comparator(sample, f.estimates[0].sample) || (time-f.estimates[2].time) > f.windowLength {
f.Reset(sample, time)
return
}
if f.comparator(sample, f.estimates[1].sample) {
f.estimates[1].sample = sample
f.estimates[1].time = time
f.estimates[2].sample = sample
f.estimates[2].time = time
} else if f.comparator(sample, f.estimates[2].sample) {
f.estimates[2].sample = sample
f.estimates[2].time = time
}
// Expire and update estimates as necessary.
if time-f.estimates[0].time > f.windowLength {
// The best estimate hasn't been updated for an entire window, so promote
// second and third best estimates.
f.estimates[0].sample = f.estimates[1].sample
f.estimates[0].time = f.estimates[1].time
f.estimates[1].sample = f.estimates[2].sample
f.estimates[1].time = f.estimates[2].time
f.estimates[2].sample = sample
f.estimates[2].time = time
// Need to iterate one more time. Check if the new best estimate is
// outside the window as well, since it may also have been recorded a
// long time ago. Don't need to iterate once more since we cover that
// case at the beginning of the method.
if time-f.estimates[0].time > f.windowLength {
f.estimates[0].sample = f.estimates[1].sample
f.estimates[0].time = f.estimates[1].time
f.estimates[1].sample = f.estimates[2].sample
f.estimates[1].time = f.estimates[2].time
}
return
}
if f.estimates[1].sample == f.estimates[0].sample && time-f.estimates[1].time > f.windowLength>>2 {
// A quarter of the window has passed without a better sample, so the
// second-best estimate is taken from the second quarter of the window.
f.estimates[1].sample = sample
f.estimates[1].time = time
f.estimates[2].sample = sample
f.estimates[2].time = time
return
}
if f.estimates[2].sample == f.estimates[1].sample && time-f.estimates[2].time > f.windowLength>>1 {
// We've passed a half of the window without a better estimate, so take
// a third-best estimate from the second half of the window.
f.estimates[2].sample = sample
f.estimates[2].time = time
}
}
func (f *WindowedFilter) Reset(newSample int64, newTime int64) {
f.estimates[0].sample = newSample
f.estimates[0].time = newTime
f.estimates[1].sample = newSample
f.estimates[1].time = newTime
f.estimates[2].sample = newSample
f.estimates[2].time = newTime
}

497
transport/tuic/packet.go Normal file
View File

@ -0,0 +1,497 @@
package tuic
import (
"bytes"
"context"
"encoding/binary"
"errors"
"io"
"math"
"net"
"os"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/atomic"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/cache"
M "github.com/sagernet/sing/common/metadata"
)
var udpMessagePool = sync.Pool{
New: func() interface{} {
return new(udpMessage)
},
}
func releaseMessages(messages []*udpMessage) {
for _, message := range messages {
if message != nil {
*message = udpMessage{}
udpMessagePool.Put(message)
}
}
}
type udpMessage struct {
sessionID uint16
packetID uint16
fragmentTotal uint8
fragmentID uint8
destination M.Socksaddr
dataLength uint16
data *buf.Buffer
}
func (m *udpMessage) release() {
*m = udpMessage{}
udpMessagePool.Put(m)
}
func (m *udpMessage) releaseMessage() {
m.data.Release()
m.release()
}
func (m *udpMessage) pack() *buf.Buffer {
buffer := buf.NewSize(m.headerSize() + m.data.Len())
common.Must(
buffer.WriteByte(Version),
buffer.WriteByte(CommandPacket),
binary.Write(buffer, binary.BigEndian, m.sessionID),
binary.Write(buffer, binary.BigEndian, m.packetID),
binary.Write(buffer, binary.BigEndian, m.fragmentTotal),
binary.Write(buffer, binary.BigEndian, m.fragmentID),
binary.Write(buffer, binary.BigEndian, uint16(m.data.Len())),
addressSerializer.WriteAddrPort(buffer, m.destination),
common.Error(buffer.Write(m.data.Bytes())),
)
return buffer
}
func (m *udpMessage) headerSize() int {
return 2 + 10 + addressSerializer.AddrPortLen(m.destination)
}
func fragUDPMessage(message *udpMessage, maxPacketSize int) []*udpMessage {
if message.data.Len() <= maxPacketSize {
return []*udpMessage{message}
}
var fragments []*udpMessage
originPacket := message.data.Bytes()
udpMTU := maxPacketSize - message.headerSize()
for remaining := len(originPacket); remaining > 0; remaining -= udpMTU {
fragment := udpMessagePool.Get().(*udpMessage)
*fragment = *message
if remaining > udpMTU {
fragment.data = buf.As(originPacket[:udpMTU])
originPacket = originPacket[udpMTU:]
} else {
fragment.data = buf.As(originPacket)
originPacket = nil
}
fragments = append(fragments, fragment)
}
fragmentTotal := uint16(len(fragments))
for index, fragment := range fragments {
fragment.fragmentID = uint8(index)
fragment.fragmentTotal = uint8(fragmentTotal)
if index > 0 {
fragment.destination = M.Socksaddr{}
}
}
return fragments
}
type udpPacketConn struct {
ctx context.Context
cancel common.ContextCancelCauseFunc
sessionID uint16
quicConn quic.Connection
data chan *udpMessage
udpStream bool
udpMTU int
packetId atomic.Uint32
closeOnce sync.Once
isServer bool
defragger *udpDefragger
onDestroy func()
}
func newUDPPacketConn(ctx context.Context, quicConn quic.Connection, udpStream bool, isServer bool, onDestroy func()) *udpPacketConn {
ctx, cancel := common.ContextWithCancelCause(ctx)
return &udpPacketConn{
ctx: ctx,
cancel: cancel,
quicConn: quicConn,
data: make(chan *udpMessage, 64),
udpStream: udpStream,
isServer: isServer,
defragger: newUDPDefragger(),
onDestroy: onDestroy,
}
}
func (c *udpPacketConn) ReadPacketThreadSafe() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
select {
case p := <-c.data:
buffer = p.data
destination = p.destination
p.release()
return
case <-c.ctx.Done():
return nil, M.Socksaddr{}, io.ErrClosedPipe
}
}
func (c *udpPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
select {
case p := <-c.data:
_, err = buffer.ReadOnceFrom(p.data)
destination = p.destination
p.releaseMessage()
return
case <-c.ctx.Done():
return M.Socksaddr{}, io.ErrClosedPipe
}
}
func (c *udpPacketConn) WaitReadPacket(newBuffer func() *buf.Buffer) (destination M.Socksaddr, err error) {
select {
case p := <-c.data:
_, err = newBuffer().ReadOnceFrom(p.data)
destination = p.destination
p.releaseMessage()
return
case <-c.ctx.Done():
return M.Socksaddr{}, io.ErrClosedPipe
}
}
func (c *udpPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
select {
case pkt := <-c.data:
n = copy(p, pkt.data.Bytes())
if pkt.destination.IsFqdn() {
addr = pkt.destination
} else {
addr = pkt.destination.UDPAddr()
}
pkt.releaseMessage()
return n, addr, nil
case <-c.ctx.Done():
return 0, nil, io.ErrClosedPipe
}
}
func (c *udpPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer buffer.Release()
select {
case <-c.ctx.Done():
return net.ErrClosed
default:
}
if buffer.Len() > 0xffff {
return quic.ErrMessageTooLarge(0xffff)
}
packetId := c.packetId.Add(1)
if packetId > math.MaxUint16 {
c.packetId.Store(0)
packetId = 0
}
message := udpMessagePool.Get().(*udpMessage)
*message = udpMessage{
sessionID: c.sessionID,
packetID: uint16(packetId),
fragmentTotal: 1,
destination: destination,
data: buffer,
}
defer message.releaseMessage()
var err error
if !c.udpStream && c.udpMTU > 0 && buffer.Len() > c.udpMTU {
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
} else {
err = c.writePacket(message)
}
if err == nil {
return nil
}
var tooLargeErr quic.ErrMessageTooLarge
if !errors.As(err, &tooLargeErr) {
return err
}
c.udpMTU = int(tooLargeErr)
return c.writePackets(fragUDPMessage(message, c.udpMTU))
}
func (c *udpPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
select {
case <-c.ctx.Done():
return 0, net.ErrClosed
default:
}
if len(p) > 0xffff {
return 0, quic.ErrMessageTooLarge(0xffff)
}
packetId := c.packetId.Add(1)
if packetId > math.MaxUint16 {
c.packetId.Store(0)
packetId = 0
}
message := udpMessagePool.Get().(*udpMessage)
*message = udpMessage{
sessionID: c.sessionID,
packetID: uint16(packetId),
fragmentTotal: 1,
destination: M.SocksaddrFromNet(addr),
data: buf.As(p),
}
if c.udpMTU > 0 && len(p) > c.udpMTU {
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
if err == nil {
return len(p), nil
}
} else {
err = c.writePacket(message)
}
if err == nil {
return len(p), nil
}
var tooLargeErr quic.ErrMessageTooLarge
if !errors.As(err, &tooLargeErr) {
return
}
c.udpMTU = int(tooLargeErr)
err = c.writePackets(fragUDPMessage(message, c.udpMTU))
if err == nil {
return len(p), nil
}
return
}
func (c *udpPacketConn) inputPacket(message *udpMessage) {
if message.fragmentTotal <= 1 {
select {
case c.data <- message:
default:
}
} else {
newMessage := c.defragger.feed(message)
if newMessage != nil {
select {
case c.data <- newMessage:
default:
}
}
}
}
func (c *udpPacketConn) writePackets(messages []*udpMessage) error {
defer releaseMessages(messages)
for _, message := range messages {
err := c.writePacket(message)
if err != nil {
return err
}
}
return nil
}
func (c *udpPacketConn) writePacket(message *udpMessage) error {
if !c.udpStream {
buffer := message.pack()
err := c.quicConn.SendMessage(buffer.Bytes())
buffer.Release()
if err != nil {
return err
}
} else {
stream, err := c.quicConn.OpenUniStream()
if err != nil {
return err
}
buffer := message.pack()
_, err = stream.Write(buffer.Bytes())
buffer.Release()
stream.Close()
if err != nil {
return err
}
}
return nil
}
func (c *udpPacketConn) Close() error {
c.closeOnce.Do(func() {
c.closeWithError(os.ErrClosed)
c.onDestroy()
})
return nil
}
func (c *udpPacketConn) closeWithError(err error) {
c.cancel(err)
if !c.isServer {
buffer := buf.NewSize(4)
defer buffer.Release()
buffer.WriteByte(Version)
buffer.WriteByte(CommandDissociate)
binary.Write(buffer, binary.BigEndian, c.sessionID)
sendStream, openErr := c.quicConn.OpenUniStream()
if openErr != nil {
return
}
defer sendStream.Close()
sendStream.Write(buffer.Bytes())
}
}
func (c *udpPacketConn) LocalAddr() net.Addr {
return c.quicConn.LocalAddr()
}
func (c *udpPacketConn) SetDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *udpPacketConn) SetReadDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *udpPacketConn) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}
type udpDefragger struct {
packetMap *cache.LruCache[uint16, *packetItem]
}
func newUDPDefragger() *udpDefragger {
return &udpDefragger{
packetMap: cache.New(
cache.WithAge[uint16, *packetItem](10),
cache.WithUpdateAgeOnGet[uint16, *packetItem](),
cache.WithEvict[uint16, *packetItem](func(key uint16, value *packetItem) {
releaseMessages(value.messages)
}),
),
}
}
type packetItem struct {
access sync.Mutex
messages []*udpMessage
count uint8
}
func (d *udpDefragger) feed(m *udpMessage) *udpMessage {
if m.fragmentTotal <= 1 {
return m
}
if m.fragmentID >= m.fragmentTotal {
return nil
}
item, _ := d.packetMap.LoadOrStore(m.packetID, newPacketItem)
item.access.Lock()
defer item.access.Unlock()
if int(m.fragmentTotal) != len(item.messages) {
releaseMessages(item.messages)
item.messages = make([]*udpMessage, m.fragmentTotal)
item.count = 1
item.messages[m.fragmentID] = m
return nil
}
if item.messages[m.fragmentID] != nil {
return nil
}
item.messages[m.fragmentID] = m
item.count++
if int(item.count) != len(item.messages) {
return nil
}
newMessage := udpMessagePool.Get().(*udpMessage)
*newMessage = *item.messages[0]
if m.dataLength > 0 {
newMessage.data = buf.NewSize(int(m.dataLength))
for _, message := range item.messages {
newMessage.data.Write(message.data.Bytes())
message.releaseMessage()
}
item.messages = nil
return newMessage
}
return nil
}
func newPacketItem() *packetItem {
return new(packetItem)
}
func readUDPMessage(message *udpMessage, reader io.Reader) error {
err := binary.Read(reader, binary.BigEndian, &message.sessionID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.packetID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.fragmentTotal)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.fragmentID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.dataLength)
if err != nil {
return err
}
message.destination, err = addressSerializer.ReadAddrPort(reader)
if err != nil {
return err
}
message.data = buf.NewSize(int(message.dataLength))
_, err = message.data.ReadFullFrom(reader, message.data.FreeLen())
if err != nil {
return err
}
return nil
}
func decodeUDPMessage(message *udpMessage, data []byte) error {
reader := bytes.NewReader(data)
err := binary.Read(reader, binary.BigEndian, &message.sessionID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.packetID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.fragmentTotal)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.fragmentID)
if err != nil {
return err
}
err = binary.Read(reader, binary.BigEndian, &message.dataLength)
if err != nil {
return err
}
message.destination, err = addressSerializer.ReadAddrPort(reader)
if err != nil {
return err
}
if reader.Len() != int(message.dataLength) {
return io.ErrUnexpectedEOF
}
message.data = buf.As(data[len(data)-reader.Len():])
return nil
}

View File

@ -0,0 +1,15 @@
package tuic
const (
Version = 5
)
const (
CommandAuthenticate = iota
CommandConnect
CommandPacket
CommandDissociate
CommandHeartbeat
)
const AuthenticateLen = 2 + 16 + 32

434
transport/tuic/server.go Normal file
View File

@ -0,0 +1,434 @@
package tuic
import (
"bytes"
"context"
"crypto/tls"
"encoding/binary"
"io"
"net"
"runtime"
"strings"
"sync"
"time"
"github.com/sagernet/quic-go"
"github.com/sagernet/sing-box/common/baderror"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid/v5"
)
type ServerOptions struct {
Context context.Context
Logger logger.Logger
TLSConfig *tls.Config
Users []User
CongestionControl string
AuthTimeout time.Duration
ZeroRTTHandshake bool
Heartbeat time.Duration
Handler ServerHandler
}
type User struct {
Name string
UUID uuid.UUID
Password string
}
type ServerHandler interface {
N.TCPConnectionHandler
N.UDPConnectionHandler
}
type Server struct {
ctx context.Context
logger logger.Logger
tlsConfig *tls.Config
heartbeat time.Duration
quicConfig *quic.Config
userMap map[uuid.UUID]User
congestionControl string
authTimeout time.Duration
handler ServerHandler
quicListener io.Closer
}
func NewServer(options ServerOptions) (*Server, error) {
if options.AuthTimeout == 0 {
options.AuthTimeout = 3 * time.Second
}
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,
Allow0RTT: options.ZeroRTTHandshake,
MaxIncomingStreams: 1 << 60,
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)
}
if len(options.Users) == 0 {
return nil, E.New("missing users")
}
userMap := make(map[uuid.UUID]User)
for _, user := range options.Users {
userMap[user.UUID] = user
}
return &Server{
ctx: options.Context,
logger: options.Logger,
tlsConfig: options.TLSConfig,
heartbeat: options.Heartbeat,
quicConfig: quicConfig,
userMap: userMap,
congestionControl: options.CongestionControl,
authTimeout: options.AuthTimeout,
handler: options.Handler,
}, nil
}
func (s *Server) Start(conn net.PacketConn) error {
if !s.quicConfig.Allow0RTT {
listener, err := quic.Listen(conn, s.tlsConfig, s.quicConfig)
if err != nil {
return err
}
s.quicListener = listener
go func() {
for {
connection, hErr := listener.Accept(s.ctx)
if hErr != nil {
if strings.Contains(hErr.Error(), "server closed") {
s.logger.Debug(E.Cause(hErr, "listener closed"))
} else {
s.logger.Error(E.Cause(hErr, "listener closed"))
}
return
}
go s.handleConnection(connection)
}
}()
} else {
listener, err := quic.ListenEarly(conn, s.tlsConfig, s.quicConfig)
if err != nil {
return err
}
s.quicListener = listener
go func() {
for {
connection, hErr := listener.Accept(s.ctx)
if hErr != nil {
if strings.Contains(hErr.Error(), "server closed") {
s.logger.Debug(E.Cause(hErr, "listener closed"))
} else {
s.logger.Error(E.Cause(hErr, "listener closed"))
}
return
}
go s.handleConnection(connection)
}
}()
}
return nil
}
func (s *Server) Close() error {
return common.Close(
s.quicListener,
)
}
func (s *Server) handleConnection(connection quic.Connection) {
setCongestion(s.ctx, connection, s.congestionControl)
session := &serverSession{
Server: s,
ctx: s.ctx,
quicConn: connection,
source: M.SocksaddrFromNet(connection.RemoteAddr()),
connDone: make(chan struct{}),
authDone: make(chan struct{}),
udpConnMap: make(map[uint16]*udpPacketConn),
}
session.handle()
}
type serverSession struct {
*Server
ctx context.Context
quicConn quic.Connection
source M.Socksaddr
connAccess sync.Mutex
connDone chan struct{}
connErr error
authDone chan struct{}
authUser *User
udpAccess sync.RWMutex
udpConnMap map[uint16]*udpPacketConn
}
func (s *serverSession) handle() {
if s.ctx.Done() != nil {
go func() {
select {
case <-s.ctx.Done():
s.closeWithError(s.ctx.Err())
case <-s.connDone:
}
}()
}
go s.loopUniStreams()
go s.loopStreams()
go s.loopMessages()
go s.handleAuthTimeout()
go s.loopHeartbeats()
}
func (s *serverSession) loopUniStreams() {
for {
uniStream, err := s.quicConn.AcceptUniStream(s.ctx)
if err != nil {
return
}
go func() {
err = s.handleUniStream(uniStream)
if err != nil {
s.closeWithError(E.Cause(err, "handle uni stream"))
}
}()
}
}
func (s *serverSession) handleUniStream(stream quic.ReceiveStream) error {
defer stream.CancelRead(0)
buffer := buf.New()
defer buffer.Release()
_, err := buffer.ReadAtLeastFrom(stream, 2)
if err != nil {
return E.Cause(err, "read request")
}
version := buffer.Byte(0)
if version != Version {
return E.New("unknown version ", buffer.Byte(0))
}
command := buffer.Byte(1)
switch command {
case CommandAuthenticate:
select {
case <-s.authDone:
return E.New("authentication: multiple authentication requests")
default:
}
if buffer.Len() < AuthenticateLen {
_, err = buffer.ReadFullFrom(stream, AuthenticateLen-buffer.Len())
if err != nil {
return E.Cause(err, "authentication: read request")
}
}
userUUID := uuid.FromBytesOrNil(buffer.Range(2, 2+16))
user, loaded := s.userMap[userUUID]
if !loaded {
return E.New("authentication: unknown user ", userUUID)
}
handshakeState := s.quicConn.ConnectionState().TLS
tuicToken, err := handshakeState.ExportKeyingMaterial(string(user.UUID[:]), []byte(user.Password), 32)
if err != nil {
return E.Cause(err, "authentication: export keying material")
}
if !bytes.Equal(tuicToken, buffer.Range(2+16, 2+16+32)) {
return E.New("authentication: token mismatch")
}
s.authUser = &user
close(s.authDone)
return nil
case CommandPacket:
select {
case <-s.connDone:
return s.connErr
case <-s.authDone:
}
message := udpMessagePool.Get().(*udpMessage)
err = readUDPMessage(message, io.MultiReader(bytes.NewReader(buffer.From(2)), stream))
if err != nil {
message.release()
return err
}
s.handleUDPMessage(message, true)
return nil
case CommandDissociate:
select {
case <-s.connDone:
return s.connErr
case <-s.authDone:
}
if buffer.Len() > 4 {
return E.New("invalid dissociate message")
}
var sessionID uint16
err = binary.Read(io.MultiReader(bytes.NewReader(buffer.From(2)), stream), binary.BigEndian, &sessionID)
if err != nil {
return err
}
s.udpAccess.RLock()
udpConn, loaded := s.udpConnMap[sessionID]
s.udpAccess.RUnlock()
if loaded {
udpConn.closeWithError(E.New("remote closed"))
s.udpAccess.Lock()
delete(s.udpConnMap, sessionID)
s.udpAccess.Unlock()
}
return nil
default:
return E.New("unknown command ", command)
}
}
func (s *serverSession) handleAuthTimeout() {
select {
case <-s.connDone:
case <-s.authDone:
case <-time.After(s.authTimeout):
s.closeWithError(E.New("authentication timeout"))
}
}
func (s *serverSession) loopStreams() {
for {
stream, err := s.quicConn.AcceptStream(s.ctx)
if err != nil {
return
}
go func() {
err = s.handleStream(stream)
if err != nil {
stream.CancelRead(0)
stream.Close()
s.logger.Error(E.Cause(err, "handle stream request"))
}
}()
}
}
func (s *serverSession) handleStream(stream quic.Stream) error {
buffer := buf.NewSize(2 + M.MaxSocksaddrLength)
defer buffer.Release()
_, err := buffer.ReadAtLeastFrom(stream, 2)
if err != nil {
return E.Cause(err, "read request")
}
version, _ := buffer.ReadByte()
if version != Version {
return E.New("unknown version ", buffer.Byte(0))
}
command, _ := buffer.ReadByte()
if command != CommandConnect {
return E.New("unsupported stream command ", command)
}
destination, err := addressSerializer.ReadAddrPort(io.MultiReader(buffer, stream))
if err != nil {
return E.Cause(err, "read request destination")
}
select {
case <-s.connDone:
return s.connErr
case <-s.authDone:
}
var conn net.Conn = &serverConn{
Stream: stream,
destination: destination,
}
if buffer.IsEmpty() {
buffer.Release()
} else {
conn = bufio.NewCachedConn(conn, buffer)
}
ctx := s.ctx
if s.authUser.Name != "" {
ctx = auth.ContextWithUser(s.ctx, s.authUser.Name)
}
_ = s.handler.NewConnection(ctx, conn, M.Metadata{
Source: s.source,
Destination: destination,
})
return nil
}
func (s *serverSession) loopHeartbeats() {
ticker := time.NewTicker(s.heartbeat)
defer ticker.Stop()
for {
select {
case <-s.connDone:
return
case <-ticker.C:
err := s.quicConn.SendMessage([]byte{Version, CommandHeartbeat})
if err != nil {
s.closeWithError(E.Cause(err, "send heartbeat"))
}
}
}
}
func (s *serverSession) closeWithError(err error) {
s.connAccess.Lock()
defer s.connAccess.Unlock()
select {
case <-s.connDone:
return
default:
s.connErr = err
close(s.connDone)
}
if E.IsClosedOrCanceled(err) {
s.logger.Debug(E.Cause(err, "connection failed"))
} else {
s.logger.Error(E.Cause(err, "connection failed"))
}
_ = s.quicConn.CloseWithError(0, "")
}
type serverConn struct {
quic.Stream
destination M.Socksaddr
}
func (c *serverConn) Read(p []byte) (n int, err error) {
n, err = c.Stream.Read(p)
return n, baderror.WrapQUIC(err)
}
func (c *serverConn) Write(p []byte) (n int, err error) {
n, err = c.Stream.Write(p)
return n, baderror.WrapQUIC(err)
}
func (c *serverConn) LocalAddr() net.Addr {
return c.destination
}
func (c *serverConn) RemoteAddr() net.Addr {
return M.Socksaddr{}
}
func (c *serverConn) Close() error {
c.Stream.CancelRead(0)
return c.Stream.Close()
}

View File

@ -0,0 +1,73 @@
package tuic
import (
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
)
func (s *serverSession) loopMessages() {
select {
case <-s.connDone:
return
case <-s.authDone:
}
for {
message, err := s.quicConn.ReceiveMessage(s.ctx)
if err != nil {
s.closeWithError(E.Cause(err, "receive message"))
return
}
hErr := s.handleMessage(message)
if hErr != nil {
s.closeWithError(E.Cause(hErr, "handle message"))
return
}
}
}
func (s *serverSession) handleMessage(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")
}
s.handleUDPMessage(message, false)
return nil
case CommandHeartbeat:
return nil
default:
return E.New("unknown command ", data[0])
}
}
func (s *serverSession) handleUDPMessage(message *udpMessage, udpStream bool) {
s.udpAccess.RLock()
udpConn, loaded := s.udpConnMap[message.sessionID]
s.udpAccess.RUnlock()
if !loaded || common.Done(udpConn.ctx) {
udpConn = newUDPPacketConn(s.ctx, s.quicConn, udpStream, true, func() {
s.udpAccess.Lock()
delete(s.udpConnMap, message.sessionID)
s.udpAccess.Unlock()
})
udpConn.sessionID = message.sessionID
s.udpAccess.Lock()
s.udpConnMap[message.sessionID] = udpConn
s.udpAccess.Unlock()
go s.handler.NewPacketConnection(udpConn.ctx, udpConn, M.Metadata{
Source: s.source,
Destination: message.destination,
})
}
udpConn.inputPacket(message)
}

View File

@ -4,6 +4,7 @@ import (
"encoding/binary"
"io"
"net"
"sync"
"github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
@ -190,6 +191,7 @@ func (c *Conn) Upstream() any {
type PacketConn struct {
net.Conn
access sync.Mutex
key [16]byte
destination M.Socksaddr
flow string
@ -218,11 +220,17 @@ func (c *PacketConn) Read(b []byte) (n int, err error) {
func (c *PacketConn) Write(b []byte) (n int, err error) {
if !c.requestWritten {
err = WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, nil)
if err == nil {
n = len(b)
c.access.Lock()
if c.requestWritten {
c.access.Unlock()
} else {
err = WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, nil)
if err == nil {
n = len(b)
}
c.requestWritten = true
c.access.Unlock()
}
c.requestWritten = true
}
err = binary.Write(c.Conn, binary.BigEndian, uint16(len(b)))
if err != nil {
@ -236,9 +244,15 @@ func (c *PacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) er
dataLen := buffer.Len()
binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(dataLen))
if !c.requestWritten {
err := WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, buffer.Bytes())
c.requestWritten = true
return err
c.access.Lock()
if c.requestWritten {
c.access.Unlock()
} else {
err := WritePacketRequest(c.Conn, Request{c.key, vmess.CommandUDP, c.destination, c.flow}, buffer.Bytes())
c.requestWritten = true
c.access.Unlock()
return err
}
}
return common.Error(c.Conn.Write(buffer.Bytes()))
}

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