Add go TUN stack

This commit is contained in:
世界 committed 2026-11-09 15:17:20 +00:00
1 parent 240682c3aa
commit 34009e2188
26 files changed
+809 -85

No files matched your search

+1
View File
@@ -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
View File
@@ -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,
+1
View File
@@ -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())
}
+20
View File
@@ -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))
})
+11
View File
@@ -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
}
+2
View File
@@ -5,6 +5,8 @@ import (
F "github.com/sagernet/sing/common/format"
)
const UDPSocketBufferSize = 8 << 20
type InterfaceType uint8
const (
+23 -3
View File
@@ -4,7 +4,9 @@ icon: material/new-box
!!! quote "Changes in sing-box 1.15.0"
:material-plus: [auto_redirect_tproxy_mark](#auto_redirect_tproxy_mark)
: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
+22 -3
View File
@@ -4,7 +4,9 @@ icon: material/new-box
!!! quote "sing-box 1.15.0 中的更改"
:material-plus: [auto_redirect_tproxy_mark](#auto_redirect_tproxy_mark)
: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
+10
View File
@@ -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
+9
View File
@@ -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` 选项
+3 -2
View File
@@ -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
+2 -4
View File
@@ -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]
+37
View File
@@ -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
+36
View File
@@ -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
View File
@@ -7889,13 +7889,8 @@
"minimum": 0,
"maximum": 4294967295
},
"stack": {
"type": "string",
"enum": [
"system",
"gvisor",
"mixed"
]
"multi_queue": {
"type": "boolean"
},
"platform": {
"$ref": "#/$defs/TunPlatformOptions"
+10
View File
@@ -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,
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+125 -29
View File
@@ -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{
Conn: conn,
manager: m,
element: element,
tracked := &trackedConn{
Conn: conn,
socketOwner: socketOwner{original: conn},
manager: m,
}
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,
manager: m,
element: element,
tracked := &trackedPacketConn{
NetPacketConn: bufio.NewPacketConn(conn),
socketOwner: socketOwner{original: conn},
manager: m,
}
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)
)
+10
View File
@@ -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
View File
@@ -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
}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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")
}
+13 -3
View File
@@ -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)
}
+101 -13
View File
@@ -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
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