mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-11-10 13:14:06 +08:00
Add go TUN stack
This commit is contained in:
26 files changed
+802
-78
No files matched your search
@@ -36,6 +36,7 @@ type NetworkManager interface {
|
||||
UpdateWIFIState(ctx context.Context)
|
||||
ResetNetwork(ctx context.Context)
|
||||
FlushSystemDNSCache()
|
||||
ReleaseMemory(ctx context.Context)
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
|
||||
+15
-12
@@ -62,7 +62,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
|
||||
var (
|
||||
dialer net.Dialer
|
||||
listener net.ListenConfig
|
||||
listenConfig net.ListenConfig
|
||||
interfaceFinder control.InterfaceFinder
|
||||
bindInterface string
|
||||
networkStrategy *C.NetworkStrategy
|
||||
@@ -77,6 +77,9 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else {
|
||||
interfaceFinder = control.NewDefaultInterfaceFinder()
|
||||
}
|
||||
socketBufferFunc := control.UDPSocketBuffer(listener.UDPSocketBufferSize())
|
||||
dialer.Control = control.Append(dialer.Control, socketBufferFunc)
|
||||
listenConfig.Control = control.Append(listenConfig.Control, socketBufferFunc)
|
||||
if options.BindInterface != "" {
|
||||
if !(C.IsLinux || C.IsDarwin || C.IsWindows) {
|
||||
return nil, E.New("`bind_interface` is only supported on Linux, macOS and Windows")
|
||||
@@ -88,7 +91,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
}
|
||||
}
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
listenConfig.Control = control.Append(listenConfig.Control, bindFunc)
|
||||
bindInterface = options.BindInterface
|
||||
}
|
||||
if options.RoutingMark > 0 {
|
||||
@@ -96,7 +99,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
return nil, E.New("`routing_mark` is only supported on Linux")
|
||||
}
|
||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||
listenConfig.Control = control.Append(listenConfig.Control, setMarkWrapper(networkManager, uint32(options.RoutingMark), false))
|
||||
}
|
||||
disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil
|
||||
if disableDefaultBind || options.TCPFastOpen {
|
||||
@@ -110,7 +113,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
if defaultOptions.BindInterface != "" && !disableDefaultBind && options.NetNs == "" {
|
||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
listenConfig.Control = control.Append(listenConfig.Control, bindFunc)
|
||||
bindInterface = defaultOptions.BindInterface
|
||||
} else if networkManager.AutoDetectInterface() && !disableDefaultBind && options.NetNs == "" {
|
||||
if platformInterface != nil && platformInterface.UsePlatformNetworkInterfaces() {
|
||||
@@ -132,7 +135,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
}
|
||||
bindFunc := networkManager.ProtectFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
listenConfig.Control = control.Append(listenConfig.Control, bindFunc)
|
||||
} else {
|
||||
bindFunc := networkManager.AutoDetectInterfaceFunc()
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
@@ -141,20 +144,20 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
}
|
||||
if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 {
|
||||
dialer.Control = control.Append(dialer.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||
listener.Control = control.Append(listener.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||
listenConfig.Control = control.Append(listenConfig.Control, setMarkWrapper(networkManager, defaultOptions.RoutingMark, true))
|
||||
}
|
||||
}
|
||||
if networkManager != nil {
|
||||
markFunc := networkManager.AutoRedirectOutputMarkFunc()
|
||||
dialer.Control = control.Append(dialer.Control, markFunc)
|
||||
listener.Control = control.Append(listener.Control, markFunc)
|
||||
listenConfig.Control = control.Append(listenConfig.Control, markFunc)
|
||||
}
|
||||
if options.ReuseAddr {
|
||||
listener.Control = control.Append(listener.Control, control.ReuseAddr())
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
if options.ProtectPath != "" {
|
||||
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
|
||||
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ProtectPath(options.ProtectPath))
|
||||
}
|
||||
if options.BindAddressNoPort {
|
||||
if !C.IsLinux {
|
||||
@@ -200,10 +203,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
}
|
||||
if udpFragment {
|
||||
dialer.Control = control.Append(dialer.Control, control.EnableUDPFragment())
|
||||
listener.Control = control.Append(listener.Control, control.EnableUDPFragment())
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.EnableUDPFragment())
|
||||
} else {
|
||||
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
|
||||
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.DisableUDPFragment())
|
||||
}
|
||||
var (
|
||||
dialer4 = dialer
|
||||
@@ -243,7 +246,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
dialer6: tcpDialer6,
|
||||
udpDialer4: udpDialer4,
|
||||
udpDialer6: udpDialer6,
|
||||
udpListener: listener,
|
||||
udpListener: listenConfig,
|
||||
udpAddr4: udpAddr4,
|
||||
udpAddr6: udpAddr6,
|
||||
netns: options.NetNs,
|
||||
|
||||
@@ -88,6 +88,7 @@ func (l *Listener) DialContext(dialer net.Dialer, ctx context.Context, network s
|
||||
|
||||
func (l *Listener) ListenPacket(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.PacketConn, error) {
|
||||
return ListenNetworkNamespace[net.PacketConn](l.ctx, l.listenOptions.NetNs, func() (net.PacketConn, error) {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.UDPSocketBuffer(UDPSocketBufferSize()))
|
||||
if l.listenOptions.BindInterface != "" {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, l.bindInterfaceFunc())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var UDPSocketBufferSize = sync.OnceValue(func() int {
|
||||
maxSocketBuffer, err := unix.SysctlUint32("kern.ipc.maxsockbuf")
|
||||
if err != nil {
|
||||
return C.UDPSocketBufferSize
|
||||
}
|
||||
// xnu sbreserve() before xnu-11215 rejects cc > sb_max * MCLBYTES / (MSIZE + MCLBYTES) with ENOBUFS instead of clamping.
|
||||
return int(uint64(maxSocketBuffer) * 2048 / (256 + 2048))
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !(darwin || dragonfly || freebsd || netbsd || openbsd)
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
func UDPSocketBufferSize() int {
|
||||
return C.UDPSocketBufferSize
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
const UDPSocketBufferSize = 8 << 20
|
||||
|
||||
type InterfaceType uint8
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,6 +5,8 @@ icon: material/new-box
|
||||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
:material-plus: [auto_redirect_tproxy_mark](#auto_redirect_tproxy_mark)
|
||||
:material-plus: [multi_queue](#multi_queue)
|
||||
:material-delete-clock: [stack](#stack)
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
@@ -123,7 +125,7 @@ icon: material/new-box
|
||||
|
||||
... // UDP NAT Fields
|
||||
|
||||
"stack": "system",
|
||||
"multi_queue": false,
|
||||
"include_interface": [
|
||||
"lan0"
|
||||
],
|
||||
@@ -168,6 +170,7 @@ icon: material/new-box
|
||||
}
|
||||
},
|
||||
// Deprecated
|
||||
"stack": "system",
|
||||
"gso": false,
|
||||
"inet4_address": [
|
||||
"172.19.0.1/30"
|
||||
@@ -563,6 +566,17 @@ to customize the mapping and filtering behavior.
|
||||
|
||||
#### stack
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.15.0"
|
||||
|
||||
`stack` is deprecated and will be removed in sing-box 1.17.0.
|
||||
Remove the `stack` option to use sing-tun's own TCP/IP stack.
|
||||
See [Migration](/migration/#migrate-tun-stack).
|
||||
|
||||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
Since 1.15.0, sing-tun uses its own TCP/IP stack, with substantial improvements over all previous
|
||||
implementations in peak performance, energy efficiency, and memory usage.
|
||||
|
||||
!!! quote "Changes in sing-box 1.8.0"
|
||||
|
||||
:material-delete-alert: The legacy LWIP stack has been deprecated and removed.
|
||||
@@ -575,7 +589,13 @@ TCP/IP stack.
|
||||
| `gvisor` | Perform L3 to L4 translation using [gVisor](https://github.com/google/gvisor)'s virtual network stack |
|
||||
| `mixed` | Mixed `system` TCP stack and `gvisor` UDP stack |
|
||||
|
||||
Defaults to the `mixed` stack if the gVisor build tag is enabled, otherwise defaults to the `system` stack.
|
||||
#### multi_queue
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, and requires sing-tun's own TCP/IP stack.
|
||||
|
||||
Enable multi-queue support based on `IFF_MULTI_QUEUE`, allowing throughput to scale with the number of CPU cores.
|
||||
|
||||
#### include_interface
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect_tproxy_mark](#auto_redirect_tproxy_mark)
|
||||
:material-plus: [multi_queue](#multi_queue)
|
||||
:material-delete-clock: [stack](#stack)
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
@@ -124,7 +126,7 @@ icon: material/new-box
|
||||
|
||||
... // UDP NAT 字段
|
||||
|
||||
"stack": "system",
|
||||
"multi_queue": false,
|
||||
"include_interface": [
|
||||
"lan0"
|
||||
],
|
||||
@@ -170,6 +172,7 @@ icon: material/new-box
|
||||
},
|
||||
|
||||
// 已弃用
|
||||
"stack": "system",
|
||||
"gso": false,
|
||||
"inet4_address": [
|
||||
"172.19.0.1/30"
|
||||
@@ -550,6 +553,16 @@ TUN 接口上 DNS 的处理方式。
|
||||
|
||||
#### stack
|
||||
|
||||
!!! failure "已在 sing-box 1.15.0 废弃"
|
||||
|
||||
`stack` 已废弃,并将在 sing-box 1.17.0 中被移除。
|
||||
移除 `stack` 参数以使用 sing-tun 自有的 TCP/IP stack。
|
||||
参阅[迁移指南](/zh/migration/#迁移-tun-stack)。
|
||||
|
||||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
自 1.15.0 起,sing-tun 使用自有 TCP/IP stack,极限性能、能效以及内存占用均大幅领先于所有旧实现。
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
|
||||
:material-delete-alert: 旧的 LWIP 栈已被弃用并移除。
|
||||
@@ -562,7 +575,13 @@ TCP/IP 栈。
|
||||
| `gvisor` | 基于 [gVisor](https://github.com/google/gvisor) 虚拟网络栈执行 L3 到 L4 转换 |
|
||||
| `mixed` | 混合 `system` TCP 栈与 `gvisor` UDP 栈 |
|
||||
|
||||
默认使用 `mixed` 栈如果 gVisor 构建标记已启用,否则默认使用 `system` 栈。
|
||||
#### multi_queue
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅在 Linux 下被支持,且需要使用 sing-tun 自有的 TCP/IP stack。
|
||||
|
||||
启用基于 `IFF_MULTI_QUEUE` 的多队列支持,使吞吐量能够随 CPU 核心数量扩展。
|
||||
|
||||
#### include_interface
|
||||
|
||||
|
||||
@@ -4,6 +4,16 @@ icon: material/delete-alert
|
||||
|
||||
# Deprecated Feature List
|
||||
|
||||
## 1.15.0
|
||||
|
||||
#### TUN `stack` option
|
||||
|
||||
The TUN `stack` option is deprecated and will be removed in sing-box 1.17.0.
|
||||
|
||||
Since 1.15.0, sing-tun uses its own TCP/IP stack, with substantial improvements over all previous
|
||||
implementations in peak performance, energy efficiency, and memory usage.
|
||||
Remove the `stack` option to use it, see [Migration](/migration/#migrate-tun-stack).
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### Legacy `download_detour` remote rule-set option
|
||||
|
||||
@@ -4,6 +4,15 @@ icon: material/delete-alert
|
||||
|
||||
# 废弃功能列表
|
||||
|
||||
## 1.15.0
|
||||
|
||||
#### TUN `stack` 参数
|
||||
|
||||
TUN 的 `stack` 参数已废弃,并将在 sing-box 1.17.0 中被移除。
|
||||
|
||||
自 1.15.0 起,sing-tun 使用自有 TCP/IP stack,极限性能、能效以及内存占用均大幅领先于所有旧实现。
|
||||
移除 `stack` 参数以使用,参阅[迁移指南](/zh/migration/#迁移-tun-stack)。
|
||||
|
||||
## 1.14.0
|
||||
|
||||
#### 旧版远程规则集 `download_detour` 选项
|
||||
|
||||
@@ -20,8 +20,9 @@ Android does not handle DHCP option 121 and is not affected.
|
||||
Update [sing-box graphical client](/clients/apple/#download) to `1.9.0-rc.16` or newer,
|
||||
then enable `includeAllNetworks` in `Settings` — `Packet Tunnel` and you will be unaffected.
|
||||
|
||||
Note: when `includeAllNetworks` is enabled, the default TUN stack is changed to `gvisor`,
|
||||
and the `system` and `mixed` stacks are not available.
|
||||
Since sing-box 1.15.0, sing-tun's own TCP/IP stack supports `includeAllNetworks`.
|
||||
Remove the `stack` option to use it. The legacy `system` and `mixed` stacks are not available
|
||||
when `includeAllNetworks` is enabled.
|
||||
|
||||
### Linux
|
||||
|
||||
|
||||
@@ -62,11 +62,9 @@ flowchart TB
|
||||
tun[TUN interface]
|
||||
windows -. route .-> tun
|
||||
linux -. iproute2 route/rule .-> tun
|
||||
tun --> gvisor[gVisor TUN stack]
|
||||
tun --> system[system TUN stack]
|
||||
tun --> stack[sing-tun TCP/IP stack]
|
||||
assemble([L3 to L4 assemble])
|
||||
gvisor --> assemble
|
||||
system --> assemble
|
||||
stack --> assemble
|
||||
assemble --> conn[TCP and UDP connections]
|
||||
conn --> router[sing-box Router]
|
||||
router --> direct[Direct outbound]
|
||||
|
||||
@@ -2,6 +2,43 @@
|
||||
icon: material/arrange-bring-forward
|
||||
---
|
||||
|
||||
## 1.15.0
|
||||
|
||||
### Migrate TUN stack
|
||||
|
||||
Since 1.15.0, sing-tun uses its own TCP/IP stack, with substantial improvements over all previous
|
||||
implementations in peak performance, energy efficiency, and memory usage.
|
||||
Remove the `stack` option to use it.
|
||||
|
||||
The `stack` option is deprecated in sing-box 1.15.0 and will be removed in sing-box 1.17.0.
|
||||
|
||||
=== ":material-card-remove: Deprecated"
|
||||
|
||||
```json
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": ["172.18.0.1/30"],
|
||||
"stack": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: Migrated"
|
||||
|
||||
```json
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": ["172.18.0.1/30"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 1.14.0
|
||||
|
||||
### Migrate the macOS standalone client data
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
icon: material/arrange-bring-forward
|
||||
---
|
||||
|
||||
## 1.15.0
|
||||
|
||||
### 迁移 TUN stack
|
||||
|
||||
自 1.15.0 起,sing-tun 使用自有 TCP/IP stack,极限性能、能效以及内存占用均大幅领先于所有旧实现。
|
||||
移除 `stack` 参数以使用。
|
||||
|
||||
`stack` 参数自 sing-box 1.15.0 起废弃,并将在 sing-box 1.17.0 中被移除。
|
||||
|
||||
=== ":material-card-remove: 已废弃"
|
||||
|
||||
```json
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": ["172.18.0.1/30"],
|
||||
"stack": "system"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-multiple: 已迁移"
|
||||
|
||||
```json
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"address": ["172.18.0.1/30"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 1.14.0
|
||||
|
||||
### 迁移 macOS standalone 客户端数据
|
||||
|
||||
+2
-7
@@ -7889,13 +7889,8 @@
|
||||
"minimum": 0,
|
||||
"maximum": 4294967295
|
||||
},
|
||||
"stack": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"system",
|
||||
"gvisor",
|
||||
"mixed"
|
||||
]
|
||||
"multi_queue": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"platform": {
|
||||
"$ref": "#/$defs/TunPlatformOptions"
|
||||
|
||||
@@ -158,6 +158,15 @@ var OptionImplicitDefaultHTTPClient = Note{
|
||||
EnvName: "IMPLICIT_DEFAULT_HTTP_CLIENT",
|
||||
}
|
||||
|
||||
var OptionTunStack = Note{
|
||||
Name: "tun-stack",
|
||||
Description: "`stack` option in TUN",
|
||||
DeprecatedVersion: "1.15.0",
|
||||
ScheduledVersion: "1.17.0",
|
||||
EnvName: "TUN_STACK",
|
||||
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-tun-stack",
|
||||
}
|
||||
|
||||
var Options = []Note{
|
||||
OptionOutboundDNSRuleItem,
|
||||
OptionMissingDomainResolver,
|
||||
@@ -170,4 +179,5 @@ var Options = []Note{
|
||||
OptionIndependentDNSCache,
|
||||
OptionStoreRDRC,
|
||||
OptionImplicitDefaultHTTPClient,
|
||||
OptionTunStack,
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func ReloadSetupOptions(options *SetupOptions) {
|
||||
debug.SetGCPercent(oomkiller.DefaultAppleNetworkExtensionGCPercent)
|
||||
}
|
||||
if sOOMMemoryLimit > 0 {
|
||||
debug.SetMemoryLimit(sOOMMemoryLimit * 4 / 5)
|
||||
debug.SetMemoryLimit(int64(oomkiller.RuntimeMemoryLimit(uint64(sOOMMemoryLimit))))
|
||||
} else {
|
||||
debug.SetMemoryLimit(math.MaxInt64)
|
||||
}
|
||||
|
||||
+3
-1
@@ -50,10 +50,12 @@ type TunInboundOptions struct {
|
||||
UDPMapping UDPNATBehavior `json:"udp_mapping,omitempty"`
|
||||
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
|
||||
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
|
||||
Stack string `json:"stack,omitempty" enum:"system,gvisor,mixed"`
|
||||
MultiQueue bool `json:"multi_queue,omitempty"`
|
||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||
InboundOptions
|
||||
|
||||
Stack string `json:"stack,omitempty" schema:"omit"`
|
||||
|
||||
// Deprecated: removed
|
||||
GSO bool `json:"gso,omitempty" schema:"omit"`
|
||||
// Deprecated: merged to Address
|
||||
|
||||
+27
-3
@@ -15,8 +15,10 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing-tun/gtcpip/header"
|
||||
"github.com/sagernet/sing/common"
|
||||
@@ -79,6 +81,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if options.InboundOptions != (option.InboundOptions{}) {
|
||||
return nil, E.New("legacy inbound fields are deprecated in sing-box 1.11.0 and removed in sing-box 1.13.0, checkout migration: https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions")
|
||||
}
|
||||
if options.Stack != "" {
|
||||
deprecated.Report(ctx, deprecated.OptionTunStack)
|
||||
}
|
||||
|
||||
address := options.Address
|
||||
inet4Address := common.Filter(address, func(it netip.Prefix) bool {
|
||||
@@ -123,7 +128,20 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
}
|
||||
var enableGSO bool
|
||||
if C.IsLinux && !usePlatformInterface {
|
||||
enableGSO = (options.Stack == "gvisor" && tunMTU < 49152)
|
||||
switch options.Stack {
|
||||
case "", "go", "gvisor":
|
||||
enableGSO = tunMTU < 49152
|
||||
}
|
||||
}
|
||||
if options.MultiQueue {
|
||||
if !C.IsLinux || usePlatformInterface {
|
||||
return nil, E.New("`multi_queue` is only supported on Linux")
|
||||
}
|
||||
switch options.Stack {
|
||||
case "", "go":
|
||||
default:
|
||||
return nil, E.New("`multi_queue` is only supported by the `go` stack")
|
||||
}
|
||||
}
|
||||
var udpTimeout time.Duration
|
||||
if options.UDPTimeout != 0 {
|
||||
@@ -180,7 +198,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
excludeMACAddress = append(excludeMACAddress, mac)
|
||||
}
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && tunMTU <= 9000))
|
||||
inbound := &Inbound{
|
||||
tag: tag,
|
||||
ctx: ctx,
|
||||
@@ -192,6 +209,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
NetNs: options.NetNs,
|
||||
MTU: tunMTU,
|
||||
GSO: enableGSO,
|
||||
MultiQueue: options.MultiQueue,
|
||||
Inet4Address: inet4Address,
|
||||
Inet6Address: inet6Address,
|
||||
DNSMode: options.DNSMode,
|
||||
@@ -224,7 +242,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
ExcludeMACAddress: excludeMACAddress,
|
||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
Logger: logger,
|
||||
EXP_MultiPendingPackets: multiPendingPackets,
|
||||
EXP_MultiPendingPackets: C.IsDarwin,
|
||||
},
|
||||
udpTimeout: udpTimeout,
|
||||
udpMapping: tun.NATMapping(options.UDPMapping),
|
||||
@@ -467,6 +485,11 @@ func (t *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
if t.platformInterface != nil && t.platformInterface.UnderNetworkExtension() {
|
||||
includeAllNetworks = t.platformInterface.NetworkExtensionIncludeAllNetworks()
|
||||
}
|
||||
var memoryPressure func() tun.MemoryPressure
|
||||
oomKiller := service.FromContext[*oomkiller.Service](t.ctx)
|
||||
if oomKiller != nil {
|
||||
memoryPressure = oomKiller.MemoryPressure
|
||||
}
|
||||
tunStack, err := tun.NewStack(t.stack, tun.StackOptions{
|
||||
Context: t.ctx,
|
||||
Tun: tunInterface,
|
||||
@@ -481,6 +504,7 @@ func (t *Inbound) Start(stage adapter.StartStage, scope *adapter.Scope) error {
|
||||
ForwarderBindInterface: C.IsDarwin,
|
||||
InterfaceFinder: t.networkManager.InterfaceFinder(),
|
||||
IncludeAllNetworks: includeAllNetworks,
|
||||
MemoryPressure: memoryPressure,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+122
-26
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/tlsspoof"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
@@ -59,6 +61,8 @@ func (m *ConnectionManager) Start(stage adapter.StartStage, scope *adapter.Scope
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) Count() int {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.connections.Len()
|
||||
}
|
||||
|
||||
@@ -78,25 +82,27 @@ func (m *ConnectionManager) CloseAll() {
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) TrackConn(conn net.Conn) net.Conn {
|
||||
m.access.Lock()
|
||||
element := m.connections.PushBack(conn)
|
||||
m.access.Unlock()
|
||||
return &trackedConn{
|
||||
tracked := &trackedConn{
|
||||
Conn: conn,
|
||||
socketOwner: socketOwner{original: conn},
|
||||
manager: m,
|
||||
element: element,
|
||||
}
|
||||
m.access.Lock()
|
||||
tracked.element = m.connections.PushBack(tracked)
|
||||
m.access.Unlock()
|
||||
return tracked
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) TrackPacketConn(conn net.PacketConn) net.PacketConn {
|
||||
m.access.Lock()
|
||||
element := m.connections.PushBack(conn)
|
||||
m.access.Unlock()
|
||||
return &trackedPacketConn{
|
||||
PacketConn: conn,
|
||||
tracked := &trackedPacketConn{
|
||||
NetPacketConn: bufio.NewPacketConn(conn),
|
||||
socketOwner: socketOwner{original: conn},
|
||||
manager: m,
|
||||
element: element,
|
||||
}
|
||||
m.access.Lock()
|
||||
tracked.element = m.connections.PushBack(tracked)
|
||||
m.access.Unlock()
|
||||
return tracked
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||
@@ -135,6 +141,19 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
|
||||
m.logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
if !metadata.TLSFragment && !metadata.TLSRecordFragment && metadata.TLSSpoof == "" {
|
||||
var spliced bool
|
||||
spliced, err = m.spliceConnection(ctx, conn, remoteConn, onClose)
|
||||
if err != nil {
|
||||
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||
remoteConn.Close()
|
||||
m.logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
if spliced {
|
||||
return
|
||||
}
|
||||
}
|
||||
if metadata.TLSFragment || metadata.TLSRecordFragment {
|
||||
remoteConn = tf.NewConn(remoteConn, ctx, metadata.TLSFragment, metadata.TLSRecordFragment, metadata.TLSFragmentFallbackDelay)
|
||||
}
|
||||
@@ -236,6 +255,18 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial
|
||||
m.logger.ErrorContext(ctx, err)
|
||||
return
|
||||
}
|
||||
udpTimeout := packetTimeout(&metadata)
|
||||
var (
|
||||
spliceRemote any = remotePacketConn
|
||||
spliced bool
|
||||
)
|
||||
if remoteConn != nil {
|
||||
spliceRemote = remoteConn
|
||||
}
|
||||
conn, spliced = m.splicePacketConnection(ctx, conn, spliceRemote, &metadata, destinationAddress, udpTimeout, onClose)
|
||||
if spliced {
|
||||
return
|
||||
}
|
||||
if destinationAddress.IsValid() {
|
||||
var originDestination M.Socksaddr
|
||||
if metadata.RouteOriginalDestination.IsValid() {
|
||||
@@ -260,18 +291,6 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial
|
||||
} else if metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination {
|
||||
remotePacketConn = bufio.NewDestinationNATPacketConn(bufio.NewPacketConn(remotePacketConn), metadata.Destination, metadata.RouteOriginalDestination)
|
||||
}
|
||||
var udpTimeout time.Duration
|
||||
if metadata.UDPTimeout > 0 {
|
||||
udpTimeout = metadata.UDPTimeout
|
||||
} else {
|
||||
protocol := metadata.Protocol
|
||||
if protocol == "" {
|
||||
protocol = C.PortProtocols[metadata.Destination.Port]
|
||||
}
|
||||
if protocol != "" {
|
||||
udpTimeout = C.ProtocolTimeouts[protocol]
|
||||
}
|
||||
}
|
||||
if udpTimeout > 0 {
|
||||
ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout)
|
||||
}
|
||||
@@ -416,16 +435,70 @@ func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.P
|
||||
common.Close(source, destination)
|
||||
}
|
||||
|
||||
type socketOwner struct {
|
||||
access sync.Mutex
|
||||
original io.Closer
|
||||
owner io.Closer
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (o *socketOwner) Attach(closer io.Closer) (io.Closer, bool) {
|
||||
o.access.Lock()
|
||||
defer o.access.Unlock()
|
||||
if o.closed || o.owner != nil {
|
||||
return nil, false
|
||||
}
|
||||
o.owner = closer
|
||||
return o.original, true
|
||||
}
|
||||
|
||||
func (o *socketOwner) detach() bool {
|
||||
o.access.Lock()
|
||||
defer o.access.Unlock()
|
||||
o.owner = nil
|
||||
return o.closed
|
||||
}
|
||||
|
||||
func (o *socketOwner) close() bool {
|
||||
o.access.Lock()
|
||||
o.closed = true
|
||||
owner := o.owner
|
||||
o.access.Unlock()
|
||||
if owner == nil {
|
||||
return false
|
||||
}
|
||||
owner.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
type trackedConn struct {
|
||||
net.Conn
|
||||
socketOwner
|
||||
manager *ConnectionManager
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func (c *trackedConn) SyscallConn() (syscall.RawConn, error) {
|
||||
syscallConn, isSyscallConn := c.Conn.(syscall.Conn)
|
||||
if !isSyscallConn {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return syscallConn.SyscallConn()
|
||||
}
|
||||
|
||||
func (c *trackedConn) Detach() {
|
||||
if c.socketOwner.detach() {
|
||||
c.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *trackedConn) Close() error {
|
||||
c.manager.access.Lock()
|
||||
c.manager.connections.Remove(c.element)
|
||||
c.manager.access.Unlock()
|
||||
if c.socketOwner.close() {
|
||||
return nil
|
||||
}
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
@@ -442,20 +515,38 @@ func (c *trackedConn) WriterReplaceable() bool {
|
||||
}
|
||||
|
||||
type trackedPacketConn struct {
|
||||
net.PacketConn
|
||||
N.NetPacketConn
|
||||
socketOwner
|
||||
manager *ConnectionManager
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) SyscallConn() (syscall.RawConn, error) {
|
||||
syscallConn, isSyscallConn := c.NetPacketConn.(syscall.Conn)
|
||||
if !isSyscallConn {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return syscallConn.SyscallConn()
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) Detach() {
|
||||
if c.socketOwner.detach() {
|
||||
c.NetPacketConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) Close() error {
|
||||
c.manager.access.Lock()
|
||||
c.manager.connections.Remove(c.element)
|
||||
c.manager.access.Unlock()
|
||||
return c.PacketConn.Close()
|
||||
if c.socketOwner.close() {
|
||||
return nil
|
||||
}
|
||||
return c.NetPacketConn.Close()
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) Upstream() any {
|
||||
return bufio.NewPacketConn(c.PacketConn)
|
||||
return c.NetPacketConn
|
||||
}
|
||||
|
||||
func (c *trackedPacketConn) ReaderReplaceable() bool {
|
||||
@@ -465,3 +556,8 @@ func (c *trackedPacketConn) ReaderReplaceable() bool {
|
||||
func (c *trackedPacketConn) WriterReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
_ tun.SpliceSocket = (*trackedConn)(nil)
|
||||
_ tun.SpliceSocket = (*trackedPacketConn)(nil)
|
||||
)
|
||||
@@ -496,6 +496,16 @@ func (r *NetworkManager) FlushSystemDNSCache() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkManager) ReleaseMemory(ctx context.Context) {
|
||||
r.ResetNetwork(ctx)
|
||||
for _, outbound := range r.outbound.Outbounds() {
|
||||
keeper, isKeeper := outbound.(adapter.IdleConnectionKeeper)
|
||||
if isKeeper {
|
||||
keeper.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkManager) notifyInterfaceUpdate(_ *control.Interface, _ int) {
|
||||
r.interfaceUpdateAccess.Lock()
|
||||
defer r.interfaceUpdateAccess.Unlock()
|
||||
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/iponly"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"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"
|
||||
)
|
||||
|
||||
type spliceTarget struct {
|
||||
socket tun.SpliceSocket
|
||||
offload N.PacketOffload
|
||||
readCounters []N.CountFunc
|
||||
writeCounters []N.CountFunc
|
||||
}
|
||||
|
||||
func unwrapSpliceTarget(conn any, allowOffload bool) (spliceTarget, bool) {
|
||||
var target spliceTarget
|
||||
for {
|
||||
readCounter, isReadCounter := conn.(N.ReadCounter)
|
||||
writeCounter, isWriteCounter := conn.(N.WriteCounter)
|
||||
if isReadCounter || isWriteCounter {
|
||||
if !isReadCounter || !isWriteCounter {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
reader, readCounters := readCounter.UnwrapReader()
|
||||
writer, writeCounters := writeCounter.UnwrapWriter()
|
||||
if any(reader) != any(writer) {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
target.readCounters = append(target.readCounters, readCounters...)
|
||||
target.writeCounters = append(target.writeCounters, writeCounters...)
|
||||
conn = reader
|
||||
continue
|
||||
}
|
||||
packetReadCounter, isPacketReadCounter := conn.(N.PacketReadCounter)
|
||||
packetWriteCounter, isPacketWriteCounter := conn.(N.PacketWriteCounter)
|
||||
if isPacketReadCounter || isPacketWriteCounter {
|
||||
if !isPacketReadCounter || !isPacketWriteCounter {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
reader, readCounters := packetReadCounter.UnwrapPacketReader()
|
||||
writer, writeCounters := packetWriteCounter.UnwrapPacketWriter()
|
||||
if any(reader) != any(writer) {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
target.readCounters = append(target.readCounters, readCounters...)
|
||||
target.writeCounters = append(target.writeCounters, writeCounters...)
|
||||
conn = reader
|
||||
continue
|
||||
}
|
||||
if allowOffload {
|
||||
upstream, offload := N.UnwrapPacketOffload(conn)
|
||||
if offload != nil {
|
||||
socket, isSocket := upstream.(tun.SpliceSocket)
|
||||
if !isSocket {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
target.socket = socket
|
||||
target.offload = offload
|
||||
return target, true
|
||||
}
|
||||
}
|
||||
ipOnlyConn, isIPOnly := conn.(*iponly.PacketConn)
|
||||
if isIPOnly {
|
||||
conn = ipOnlyConn.Upstream()
|
||||
continue
|
||||
}
|
||||
readerWithUpstream, isReaderWithUpstream := conn.(N.ReaderWithUpstream)
|
||||
if !isReaderWithUpstream || !readerWithUpstream.ReaderReplaceable() {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
writerWithUpstream, isWriterWithUpstream := conn.(N.WriterWithUpstream)
|
||||
if !isWriterWithUpstream || !writerWithUpstream.WriterReplaceable() {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
socket, isSocket := conn.(tun.SpliceSocket)
|
||||
if isSocket {
|
||||
target.socket = socket
|
||||
return target, true
|
||||
}
|
||||
withUpstream, hasUpstream := conn.(common.WithUpstream)
|
||||
if hasUpstream {
|
||||
conn = withUpstream.Upstream()
|
||||
continue
|
||||
}
|
||||
upstreamReader, hasUpstreamReader := conn.(N.WithUpstreamReader)
|
||||
upstreamWriter, hasUpstreamWriter := conn.(N.WithUpstreamWriter)
|
||||
if !hasUpstreamReader || !hasUpstreamWriter {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
reader := upstreamReader.UpstreamReader()
|
||||
if reader != upstreamWriter.UpstreamWriter() {
|
||||
return spliceTarget{}, false
|
||||
}
|
||||
conn = reader
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) spliceClose(ctx context.Context, conn io.Closer, remote io.Closer, onClose N.CloseHandlerFunc) N.CloseHandlerFunc {
|
||||
return func(err error) {
|
||||
if err == nil {
|
||||
m.logger.DebugContext(ctx, "connection finished")
|
||||
} else if !E.IsClosedOrCanceled(err) {
|
||||
m.logger.ErrorContext(ctx, "connection closed: ", err)
|
||||
} else {
|
||||
m.logger.TraceContext(ctx, "connection closed")
|
||||
}
|
||||
if onClose != nil {
|
||||
onClose(err)
|
||||
}
|
||||
common.Close(conn, remote)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) spliceConnection(ctx context.Context, conn net.Conn, remoteConn net.Conn, onClose N.CloseHandlerFunc) (bool, error) {
|
||||
destination, writeCounters := N.UnwrapCountWriter(conn, nil)
|
||||
goConn, isGoConn := N.CastWriter[*tun.GoConn](destination)
|
||||
if !isGoConn {
|
||||
return false, nil
|
||||
}
|
||||
target, isTarget := unwrapSpliceTarget(remoteConn, false)
|
||||
if !isTarget {
|
||||
return false, nil
|
||||
}
|
||||
var (
|
||||
source io.Reader = conn
|
||||
readCounters []N.CountFunc
|
||||
)
|
||||
for {
|
||||
source, readCounters = N.UnwrapCountReader(source, readCounters)
|
||||
cachedReader, isCached := source.(N.CachedReader)
|
||||
if !isCached {
|
||||
break
|
||||
}
|
||||
buffer := cachedReader.ReadCached()
|
||||
if buffer == nil {
|
||||
break
|
||||
}
|
||||
dataLen := buffer.Len()
|
||||
_, err := remoteConn.Write(buffer.Bytes())
|
||||
buffer.Release()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, counter := range readCounters {
|
||||
counter(int64(dataLen))
|
||||
}
|
||||
}
|
||||
goReader, isGoReader := N.CastReader[*tun.GoConn](source)
|
||||
if !isGoReader || goReader != goConn {
|
||||
return false, nil
|
||||
}
|
||||
return goConn.Splice(target.socket, tun.SpliceOptions{
|
||||
ReadCounters: append(readCounters, target.writeCounters...),
|
||||
WriteCounters: append(target.readCounters, writeCounters...),
|
||||
OnClose: m.spliceClose(ctx, conn, remoteConn, onClose),
|
||||
}), nil
|
||||
}
|
||||
|
||||
type spliceSource struct {
|
||||
natConn *tun.UDPNatConn
|
||||
cachedReaders []N.CachedPacketReader
|
||||
readCounters []N.CountFunc
|
||||
writeCounters []N.CountFunc
|
||||
}
|
||||
|
||||
func unwrapSpliceSource(conn N.PacketConn) (spliceSource, bool) {
|
||||
var source spliceSource
|
||||
writer, writeCounters := N.UnwrapCountPacketWriter(conn, nil)
|
||||
natWriter, isNATWriter := N.CastPacketWriter[*tun.UDPNatConn](writer)
|
||||
if !isNATWriter {
|
||||
return spliceSource{}, false
|
||||
}
|
||||
source.writeCounters = writeCounters
|
||||
var reader N.PacketReader = conn
|
||||
for {
|
||||
readCounter, isReadCounter := reader.(N.PacketReadCounter)
|
||||
if isReadCounter {
|
||||
upstreamReader, readCounters := readCounter.UnwrapPacketReader()
|
||||
source.readCounters = append(source.readCounters, readCounters...)
|
||||
reader = upstreamReader
|
||||
continue
|
||||
}
|
||||
natReader, isNATReader := reader.(*tun.UDPNatConn)
|
||||
if isNATReader {
|
||||
if natReader != natWriter {
|
||||
return spliceSource{}, false
|
||||
}
|
||||
source.natConn = natReader
|
||||
return source, true
|
||||
}
|
||||
cachedReader, isCached := reader.(N.CachedPacketReader)
|
||||
if isCached {
|
||||
source.cachedReaders = append(source.cachedReaders, cachedReader)
|
||||
} else {
|
||||
readerWithUpstream, isReaderWithUpstream := reader.(N.ReaderWithUpstream)
|
||||
if !isReaderWithUpstream || !readerWithUpstream.ReaderReplaceable() {
|
||||
return spliceSource{}, false
|
||||
}
|
||||
}
|
||||
withUpstream, hasUpstream := reader.(common.WithUpstream)
|
||||
if hasUpstream {
|
||||
reader, _ = withUpstream.Upstream().(N.PacketReader)
|
||||
} else {
|
||||
upstreamReader, hasUpstreamReader := reader.(N.WithUpstreamReader)
|
||||
if !hasUpstreamReader {
|
||||
return spliceSource{}, false
|
||||
}
|
||||
reader, _ = upstreamReader.UpstreamReader().(N.PacketReader)
|
||||
}
|
||||
if reader == nil {
|
||||
return spliceSource{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *spliceSource) takeCached() []*N.PacketBuffer {
|
||||
var cached []*N.PacketBuffer
|
||||
for _, cachedReader := range s.cachedReaders {
|
||||
packet := cachedReader.ReadCachedPacket()
|
||||
if packet == nil {
|
||||
continue
|
||||
}
|
||||
if packet.Buffer == nil {
|
||||
N.PutPacketBuffer(packet)
|
||||
continue
|
||||
}
|
||||
cached = append(cached, packet)
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) splicePacketConnection(ctx context.Context, conn N.PacketConn, remote any, metadata *adapter.InboundContext, destinationAddress netip.Addr, udpTimeout time.Duration, onClose N.CloseHandlerFunc) (N.PacketConn, bool) {
|
||||
var nat tun.PacketNAT
|
||||
source := conn
|
||||
fakeIPConn, isFakeIP := conn.(*fakeIPNATPacketConn)
|
||||
if isFakeIP {
|
||||
source = fakeIPConn.NetPacketConn
|
||||
}
|
||||
spliceSource, isSpliceSource := unwrapSpliceSource(source)
|
||||
if !isSpliceSource {
|
||||
return conn, false
|
||||
}
|
||||
target, isTarget := unwrapSpliceTarget(remote, true)
|
||||
if !isTarget {
|
||||
return conn, false
|
||||
}
|
||||
if destinationAddress.IsValid() {
|
||||
nat.Destination = M.SocksaddrFrom(destinationAddress, metadata.Destination.Port)
|
||||
} else {
|
||||
nat.Destination = metadata.Destination
|
||||
}
|
||||
switch {
|
||||
case isFakeIP:
|
||||
nat.Origin = metadata.OriginDestination
|
||||
nat.FakeIP = true
|
||||
case metadata.RouteOriginalDestination.IsValid() && metadata.RouteOriginalDestination != metadata.Destination:
|
||||
nat.Origin = metadata.RouteOriginalDestination
|
||||
case destinationAddress.IsValid() && metadata.Destination.IsIP():
|
||||
nat.Origin = metadata.Destination
|
||||
}
|
||||
nat.Unidirectional = !isFakeIP && metadata.UDPDisableDomainUnmapping && !metadata.Destination.IsIP()
|
||||
cached := spliceSource.takeCached()
|
||||
if spliceSource.natConn.Splice(target.socket, tun.SplicePacketOptions{
|
||||
SpliceOptions: tun.SpliceOptions{
|
||||
ReadCounters: append(spliceSource.readCounters, target.writeCounters...),
|
||||
WriteCounters: append(target.readCounters, spliceSource.writeCounters...),
|
||||
OnClose: m.spliceClose(ctx, conn, remote.(io.Closer), onClose),
|
||||
},
|
||||
Timeout: udpTimeout,
|
||||
NAT: nat,
|
||||
Cached: cached,
|
||||
Offload: target.offload,
|
||||
FrontHeadroom: N.CalculateFrontHeadroom(remote),
|
||||
RearHeadroom: N.CalculateRearHeadroom(remote),
|
||||
}) {
|
||||
return conn, true
|
||||
}
|
||||
if len(cached) == 0 {
|
||||
return conn, false
|
||||
}
|
||||
for _, packet := range slices.Backward(cached) {
|
||||
for _, counter := range spliceSource.readCounters {
|
||||
counter(int64(packet.Buffer.Len()))
|
||||
}
|
||||
source = bufio.NewCachedPacketConn(source, packet.Buffer, packet.Destination)
|
||||
N.PutPacketBuffer(packet)
|
||||
}
|
||||
if isFakeIP {
|
||||
fakeIPConn.NetPacketConn = bufio.NewNetPacketConn(source)
|
||||
return conn, false
|
||||
}
|
||||
return source, false
|
||||
}
|
||||
|
||||
func packetTimeout(metadata *adapter.InboundContext) time.Duration {
|
||||
if metadata.UDPTimeout > 0 {
|
||||
return metadata.UDPTimeout
|
||||
}
|
||||
protocol := metadata.Protocol
|
||||
if protocol == "" {
|
||||
protocol = C.PortProtocols[metadata.Destination.Port]
|
||||
}
|
||||
if protocol != "" {
|
||||
return C.ProtocolTimeouts[protocol]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func PromoteDraft(basePath string, accept func(metadataContent []byte) bool) {
|
||||
return
|
||||
}
|
||||
}
|
||||
promoteDirectory(draftPath, filepath.Join(basePath, ReportsDirectoryName))
|
||||
promoteDirectory(draftPath, filepath.Join(basePath, ReportsDirectoryName), info.ModTime().UTC())
|
||||
}
|
||||
|
||||
func draftNotable(draftPath string) bool {
|
||||
@@ -64,7 +64,7 @@ func draftNotable(draftPath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func promoteDirectory(draftPath string, reportsPath string) {
|
||||
func promoteDirectory(draftPath string, reportsPath string, timestamp time.Time) {
|
||||
info, err := os.Stat(draftPath)
|
||||
if err != nil || !info.IsDir() {
|
||||
return
|
||||
@@ -73,7 +73,7 @@ func promoteDirectory(draftPath string, reportsPath string) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
destPath, err := nextAvailableReportPath(reportsPath, info.ModTime().UTC())
|
||||
destPath, err := nextAvailableReportPath(reportsPath, timestamp)
|
||||
if err != nil {
|
||||
os.RemoveAll(draftPath)
|
||||
return
|
||||
|
||||
@@ -222,7 +222,7 @@ func (r *Recorder) WriteReport() error {
|
||||
sample.availableKnown = true
|
||||
sample.available = memory.Available()
|
||||
}
|
||||
err := r.snapshot(SnapshotReasonManual, sample, false, true)
|
||||
err := r.snapshot(SnapshotReasonManual, sample, true, true)
|
||||
if err != nil {
|
||||
return E.Cause(err, "write snapshot")
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package oomkiller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||
boxConstant "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
@@ -25,6 +27,7 @@ type Service struct {
|
||||
recorder *Recorder
|
||||
timerConfig timerConfig
|
||||
adaptiveTimer *adaptiveTimer
|
||||
pressure atomic.Uint32
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.OOMKillerServiceOptions) (adapter.Service, error) {
|
||||
@@ -33,7 +36,7 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Service{
|
||||
s := &Service{
|
||||
Adapter: boxService.NewAdapter(boxConstant.TypeOOMKiller, tag),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
@@ -41,14 +44,20 @@ func NewService(ctx context.Context, logger log.ContextLogger, tag string, optio
|
||||
connections: service.FromContext[adapter.ConnectionManager](ctx),
|
||||
recorder: service.FromContext[*Recorder](ctx),
|
||||
timerConfig: config,
|
||||
}, nil
|
||||
}
|
||||
service.MustRegister[*Service](ctx, s)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Service) MemoryPressure() tun.MemoryPressure {
|
||||
return tun.MemoryPressure(s.pressure.Load())
|
||||
}
|
||||
|
||||
func (s *Service) startTimer() error {
|
||||
if !s.timerConfig.policyMode.hasTimerMode() {
|
||||
return E.New("memory pressure monitoring is not available on this platform without memory_limit")
|
||||
}
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.connections, service.FromContext[adapter.CacheFile](s.ctx), s.recorder, s.timerConfig)
|
||||
s.adaptiveTimer = newAdaptiveTimer(s.logger, s.network, s.connections, service.FromContext[adapter.CacheFile](s.ctx), s.recorder, &s.pressure, s.timerConfig)
|
||||
var carriedState *timerState
|
||||
if s.recorder != nil {
|
||||
carriedState = s.recorder.instanceStarted(s.timerConfig, s.adaptiveTimer.limitThresholds)
|
||||
@@ -59,6 +68,7 @@ func (s *Service) startTimer() error {
|
||||
|
||||
func (s *Service) stopTimer() {
|
||||
state := s.adaptiveTimer.stop()
|
||||
s.pressure.Store(uint32(tun.MemoryPressureNone))
|
||||
if s.recorder != nil {
|
||||
s.recorder.instanceStopped(state)
|
||||
}
|
||||
|
||||
+99
-11
@@ -2,13 +2,17 @@ package oomkiller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
runtimeDebug "runtime/debug"
|
||||
"runtime/metrics"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
@@ -16,8 +20,11 @@ import (
|
||||
|
||||
const (
|
||||
defaultMinInterval = 100 * time.Millisecond
|
||||
defaultArmedInterval = time.Second
|
||||
defaultMaxInterval = 10 * time.Second
|
||||
defaultReleaseInterval = time.Second
|
||||
defaultReleaseShrinkFloor = 16
|
||||
defaultReleaseShrinkDivisor = 10
|
||||
defaultMaxRateLookahead = time.Second
|
||||
defaultSafetyMargin = 5 * 1024 * 1024
|
||||
defaultAvailableTriggerMarginMin = 32 * 1024 * 1024
|
||||
defaultAvailableTriggerMarginMax = 128 * 1024 * 1024
|
||||
@@ -40,6 +47,7 @@ type memorySample struct {
|
||||
type pressureThresholds struct {
|
||||
trigger uint64
|
||||
armed uint64
|
||||
resume uint64
|
||||
}
|
||||
|
||||
type timerConfig struct {
|
||||
@@ -47,7 +55,6 @@ type timerConfig struct {
|
||||
safetyMargin uint64
|
||||
hasSafetyMargin bool
|
||||
minInterval time.Duration
|
||||
armedInterval time.Duration
|
||||
maxInterval time.Duration
|
||||
policyMode policyMode
|
||||
killerDisabled bool
|
||||
@@ -90,7 +97,6 @@ func buildTimerConfig(options option.OOMKillerServiceOptions, memoryLimit uint64
|
||||
safetyMargin: safetyMargin,
|
||||
hasSafetyMargin: hasSafetyMargin,
|
||||
minInterval: minInterval,
|
||||
armedInterval: max(min(defaultArmedInterval, maxInterval), minInterval),
|
||||
maxInterval: maxInterval,
|
||||
policyMode: policyMode,
|
||||
killerDisabled: killerDisabled,
|
||||
@@ -113,14 +119,20 @@ type adaptiveTimer struct {
|
||||
connections adapter.ConnectionManager
|
||||
cacheFile adapter.CacheFile
|
||||
recorder *Recorder
|
||||
pressure *atomic.Uint32
|
||||
limitThresholds pressureThresholds
|
||||
|
||||
access sync.Mutex
|
||||
timer *time.Timer
|
||||
lastGoroutines int
|
||||
lastConnections int
|
||||
lastGCCycles uint64
|
||||
releaseGCCycles uint64
|
||||
lastRelease time.Time
|
||||
timerState
|
||||
}
|
||||
|
||||
func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, connections adapter.ConnectionManager, cacheFile adapter.CacheFile, recorder *Recorder, config timerConfig) *adaptiveTimer {
|
||||
func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager, connections adapter.ConnectionManager, cacheFile adapter.CacheFile, recorder *Recorder, pressure *atomic.Uint32, config timerConfig) *adaptiveTimer {
|
||||
t := &adaptiveTimer{
|
||||
timerConfig: config,
|
||||
logger: logger,
|
||||
@@ -128,6 +140,7 @@ func newAdaptiveTimer(logger log.ContextLogger, network adapter.NetworkManager,
|
||||
connections: connections,
|
||||
cacheFile: cacheFile,
|
||||
recorder: recorder,
|
||||
pressure: pressure,
|
||||
}
|
||||
if config.policyMode == policyModeMemoryLimit || config.policyMode == policyModeNetworkExtension {
|
||||
t.limitThresholds = computeLimitThresholds(config.memoryLimit, config.safetyMargin)
|
||||
@@ -191,7 +204,8 @@ func (t *adaptiveTimer) poll() {
|
||||
t.pressureBaselineTime = time.Time{}
|
||||
}
|
||||
}
|
||||
t.timer.Reset(t.intervalForState())
|
||||
interval := t.intervalForState()
|
||||
t.timer.Reset(interval)
|
||||
triggered = previousState != pressureStateTriggered && t.state == pressureStateTriggered
|
||||
if !triggered && !t.pressureBaselineTime.IsZero() && t.memoryLimit > 0 &&
|
||||
sample.usage > t.pressureBaseline.usage && sample.usage < t.memoryLimit {
|
||||
@@ -201,7 +215,8 @@ func (t *adaptiveTimer) poll() {
|
||||
ratePerSecond := float64(growth) / elapsed.Seconds()
|
||||
headroom := t.memoryLimit - sample.usage
|
||||
secondsUntilLimit := float64(headroom) / ratePerSecond
|
||||
if secondsUntilLimit < t.minInterval.Seconds() {
|
||||
lookahead := min(2*interval, defaultMaxRateLookahead)
|
||||
if secondsUntilLimit < lookahead.Seconds() {
|
||||
triggered = true
|
||||
rateTriggered = true
|
||||
t.state = pressureStateTriggered
|
||||
@@ -210,6 +225,7 @@ func (t *adaptiveTimer) poll() {
|
||||
}
|
||||
state := t.state
|
||||
t.access.Unlock()
|
||||
t.pressure.Store(uint32(state.memoryPressure()))
|
||||
var connections int
|
||||
if t.connections != nil {
|
||||
connections = t.connections.Count()
|
||||
@@ -221,6 +237,7 @@ func (t *adaptiveTimer) poll() {
|
||||
}
|
||||
}
|
||||
if !triggered {
|
||||
t.releaseIdleMemory(sample, connections)
|
||||
return
|
||||
}
|
||||
var reason string
|
||||
@@ -230,7 +247,7 @@ func (t *adaptiveTimer) poll() {
|
||||
t.logger.Warn("memory growth rate critical (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
|
||||
} else {
|
||||
t.logger.Error("memory growth rate critical, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network")
|
||||
t.network.ResetNetwork(context.Background())
|
||||
t.network.ReleaseMemory(context.Background())
|
||||
}
|
||||
} else {
|
||||
reason = resetReasonThreshold
|
||||
@@ -238,7 +255,7 @@ func (t *adaptiveTimer) poll() {
|
||||
t.logger.Warn("memory threshold reached (report only), usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample))
|
||||
} else {
|
||||
t.logger.Error("memory threshold reached, usage: ", byteformats.FormatMemoryBytes(sample.usage), t.logDetails(sample), ", resetting network")
|
||||
t.network.ResetNetwork(context.Background())
|
||||
t.network.ReleaseMemory(context.Background())
|
||||
}
|
||||
}
|
||||
t.releaseMemory()
|
||||
@@ -249,6 +266,57 @@ func (t *adaptiveTimer) poll() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) releaseIdleMemory(sample memorySample, connections int) {
|
||||
goroutines := runtime.NumGoroutine()
|
||||
gcCycles := readGCCycles()
|
||||
now := time.Now()
|
||||
t.access.Lock()
|
||||
shrank := hasMeaningfulDrop(t.lastGoroutines, goroutines) || hasMeaningfulDrop(t.lastConnections, connections)
|
||||
idleGC := gcCycles == t.lastGCCycles && gcCycles != t.releaseGCCycles
|
||||
t.lastGoroutines = goroutines
|
||||
t.lastConnections = connections
|
||||
t.lastGCCycles = gcCycles
|
||||
if t.belowResume(sample) || now.Sub(t.lastRelease) < defaultReleaseInterval || (!shrank && !idleGC) {
|
||||
t.access.Unlock()
|
||||
return
|
||||
}
|
||||
t.lastRelease = now
|
||||
t.access.Unlock()
|
||||
runtimeDebug.FreeOSMemory()
|
||||
t.access.Lock()
|
||||
t.lastGCCycles = readGCCycles()
|
||||
t.releaseGCCycles = t.lastGCCycles
|
||||
t.access.Unlock()
|
||||
t.logger.Trace("released idle memory, usage: ", byteformats.FormatMemoryBytes(sample.usage), " -> ", byteformats.FormatMemoryBytes(memory.Total()))
|
||||
}
|
||||
|
||||
func hasMeaningfulDrop(previous int, current int) bool {
|
||||
if current >= previous {
|
||||
return false
|
||||
}
|
||||
return previous-current >= max(defaultReleaseShrinkFloor, previous/defaultReleaseShrinkDivisor)
|
||||
}
|
||||
|
||||
func readGCCycles() uint64 {
|
||||
samples := []metrics.Sample{{Name: "/gc/cycles/total:gc-cycles"}}
|
||||
metrics.Read(samples)
|
||||
if samples[0].Value.Kind() != metrics.KindUint64 {
|
||||
return 0
|
||||
}
|
||||
return samples[0].Value.Uint64()
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) belowResume(sample memorySample) bool {
|
||||
switch t.policyMode {
|
||||
case policyModeMemoryLimit, policyModeNetworkExtension:
|
||||
return sample.usage < t.limitThresholds.resume
|
||||
case policyModeAvailable:
|
||||
return !sample.availableKnown || sample.available > t.availableThresholds(sample).resume
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) belowTrigger(sample memorySample) bool {
|
||||
switch t.policyMode {
|
||||
case policyModeMemoryLimit, policyModeNetworkExtension:
|
||||
@@ -264,8 +332,23 @@ func (t *adaptiveTimer) releaseMemory() {
|
||||
if t.cacheFile != nil {
|
||||
t.cacheFile.Flush()
|
||||
}
|
||||
t.pressure.Store(uint32(tun.MemoryPressureCritical))
|
||||
badCleanup()
|
||||
runtimeDebug.FreeOSMemory()
|
||||
t.access.Lock()
|
||||
t.releaseGCCycles = readGCCycles()
|
||||
t.access.Unlock()
|
||||
}
|
||||
|
||||
func (s pressureState) memoryPressure() tun.MemoryPressure {
|
||||
switch s {
|
||||
case pressureStateArmed:
|
||||
return tun.MemoryPressureWarning
|
||||
case pressureStateTriggered:
|
||||
return tun.MemoryPressureCritical
|
||||
default:
|
||||
return tun.MemoryPressureNone
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) nextState(sample memorySample) pressureState {
|
||||
@@ -292,12 +375,18 @@ func (t *adaptiveTimer) nextState(sample memorySample) pressureState {
|
||||
func computeLimitThresholds(memoryLimit uint64, safetyMargin uint64) pressureThresholds {
|
||||
triggerMargin := min(safetyMargin, memoryLimit)
|
||||
armedMargin := min(triggerMargin*2, memoryLimit)
|
||||
resumeMargin := min(triggerMargin*4, memoryLimit)
|
||||
return pressureThresholds{
|
||||
trigger: memoryLimit - triggerMargin,
|
||||
armed: memoryLimit - armedMargin,
|
||||
resume: memoryLimit - resumeMargin,
|
||||
}
|
||||
}
|
||||
|
||||
func RuntimeMemoryLimit(memoryLimit uint64) uint64 {
|
||||
return computeLimitThresholds(memoryLimit, defaultSafetyMargin).armed
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresholds {
|
||||
var triggerMargin uint64
|
||||
if t.hasSafetyMargin {
|
||||
@@ -310,15 +399,14 @@ func (t *adaptiveTimer) availableThresholds(sample memorySample) pressureThresho
|
||||
return pressureThresholds{
|
||||
trigger: triggerMargin,
|
||||
armed: triggerMargin * 2,
|
||||
resume: triggerMargin * 4,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *adaptiveTimer) intervalForState() time.Duration {
|
||||
switch {
|
||||
case t.forceMinInterval || t.state == pressureStateTriggered:
|
||||
case t.forceMinInterval || t.state != pressureStateNormal || !t.pressureBaselineTime.IsZero():
|
||||
t.currentInterval = t.minInterval
|
||||
case t.state == pressureStateArmed:
|
||||
t.currentInterval = t.armedInterval
|
||||
default:
|
||||
if t.currentInterval == 0 {
|
||||
t.currentInterval = t.maxInterval
|
||||
|
||||
Reference in new issue
Block a user