mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-10-23 21:01:50 +00:00
Improve idle connection management
This commit is contained in:
parent
6b0ca9a24d
commit
7ad8567e67
26 changed files with 541 additions and 37 deletions
|
|
@ -14,6 +14,12 @@ type Endpoint interface {
|
|||
Outbound
|
||||
}
|
||||
|
||||
type OnDemandEndpoint interface {
|
||||
Endpoint
|
||||
OnDemand() bool
|
||||
SetKeepIdleConnections(keep bool)
|
||||
}
|
||||
|
||||
type EndpointRegistry interface {
|
||||
option.EndpointOptionsRegistry
|
||||
Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-anytls"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-mux"
|
||||
|
|
@ -85,6 +86,7 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (uint16, error)
|
|||
if isMultiplexOutbound && multiplexOutbound.MultiplexEnabled() {
|
||||
warmContext := adapter.ContextWithKeepSession(ctx)
|
||||
warmContext = mux.ContextWithKeepSession(warmContext)
|
||||
warmContext = anytls.ContextWithKeepSession(warmContext)
|
||||
warmContext = contextWithQUICKeepSession(warmContext)
|
||||
warmContext = snell.ContextWithKeepSession(warmContext)
|
||||
_, err := urlTest(warmContext, link, detour)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
==Client only==
|
||||
|
||||
## Structure
|
||||
|
|
@ -107,6 +111,7 @@
|
|||
"promote": false
|
||||
}
|
||||
],
|
||||
"on_demand": false,
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
|
|
@ -620,6 +625,12 @@ Ask for the matching authentication field interactively instead of supplying an
|
|||
|
||||
Conflict with `form_entries.value`.
|
||||
|
||||
### on_demand
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
Allow the endpoint to be disconnected when necessary.
|
||||
|
||||
## UDP NAT Fields
|
||||
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
==仅客户端==
|
||||
|
||||
## 结构
|
||||
|
|
@ -107,6 +111,7 @@
|
|||
"promote": false
|
||||
}
|
||||
],
|
||||
"on_demand": false,
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
|
|
@ -620,6 +625,12 @@ MCA 证书和私钥必须同时设置或同时为空。
|
|||
|
||||
与 `form_entries.value` 冲突。
|
||||
|
||||
### on_demand
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
允许该 endpoint 在需要时断开连接。
|
||||
|
||||
## UDP NAT 字段
|
||||
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
## Structure
|
||||
|
||||
```json
|
||||
|
|
@ -100,6 +104,7 @@
|
|||
"system": false,
|
||||
"name": "",
|
||||
"mtu": 1500,
|
||||
"on_demand": false,
|
||||
|
||||
... // UDP NAT Fields
|
||||
|
||||
|
|
@ -684,6 +689,12 @@ OpenVPN interface MTU.
|
|||
|
||||
When empty, `1500` is used until a server-pushed MTU is received.
|
||||
|
||||
### on_demand
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
Allow the endpoint to be disconnected when necessary.
|
||||
|
||||
## UDP NAT Fields
|
||||
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
## 结构
|
||||
|
||||
```json
|
||||
|
|
@ -100,6 +104,7 @@
|
|||
"system": false,
|
||||
"name": "",
|
||||
"mtu": 1500,
|
||||
"on_demand": false,
|
||||
|
||||
... // UDP NAT 字段
|
||||
|
||||
|
|
@ -670,6 +675,12 @@ OpenVPN 接口 MTU。
|
|||
|
||||
为空时使用服务器推送的 MTU;收到服务器配置前使用 `1500`。
|
||||
|
||||
### on_demand
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
允许该 endpoint 在需要时断开连接。
|
||||
|
||||
## UDP NAT 字段
|
||||
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [listen_port](#listen_port)
|
||||
|
|
@ -45,6 +49,7 @@ icon: material/new-box
|
|||
"udp_timeout": "5m",
|
||||
"ssh_server": false,
|
||||
"taildrop_directory": "",
|
||||
"on_demand": false,
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
|
|
@ -224,6 +229,12 @@ is.
|
|||
|
||||
`Taildrop` is used by default.
|
||||
|
||||
#### on_demand
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
Allow the endpoint to be disconnected when necessary.
|
||||
|
||||
### Dial Fields
|
||||
|
||||
!!! note
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [listen_port](#listen_port)
|
||||
|
|
@ -45,6 +49,7 @@ icon: material/new-box
|
|||
"udp_timeout": "5m",
|
||||
"ssh_server": false,
|
||||
"taildrop_directory": "",
|
||||
"on_demand": false,
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
|
|
@ -222,6 +227,12 @@ UDP NAT 过期时间。
|
|||
|
||||
默认使用 `Taildrop`。
|
||||
|
||||
#### on_demand
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
允许该 endpoint 在需要时断开连接。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
!!! note
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
!!! quote "Changes in sing-box 1.15.0"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [udp_mapping](/configuration/shared/udp-nat/#udp_mapping)
|
||||
|
|
@ -34,6 +38,7 @@
|
|||
... // UDP NAT Fields
|
||||
|
||||
"workers": 0,
|
||||
"on_demand": false,
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
|
|
@ -126,6 +131,12 @@ WireGuard worker count.
|
|||
|
||||
CPU count is used by default.
|
||||
|
||||
#### on_demand
|
||||
|
||||
!!! question "Since sing-box 1.15.0"
|
||||
|
||||
Allow the endpoint to be disconnected when necessary.
|
||||
|
||||
### UDP NAT Fields
|
||||
|
||||
See [UDP NAT Fields](/configuration/shared/udp-nat/) for details.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
!!! quote "sing-box 1.15.0 中的更改"
|
||||
|
||||
:material-plus: [on_demand](#on_demand)
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [udp_mapping](/zh/configuration/shared/udp-nat/#udp_mapping)
|
||||
|
|
@ -34,6 +38,7 @@
|
|||
... // UDP NAT 字段
|
||||
|
||||
"workers": 0,
|
||||
"on_demand": false,
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
|
|
@ -128,6 +133,12 @@ WireGuard worker 数量。
|
|||
|
||||
默认使用 CPU 数量。
|
||||
|
||||
#### on_demand
|
||||
|
||||
!!! question "自 sing-box 1.15.0 起"
|
||||
|
||||
允许该 endpoint 在需要时断开连接。
|
||||
|
||||
### UDP NAT 字段
|
||||
|
||||
参阅 [UDP NAT 字段](/zh/configuration/shared/udp-nat/)。
|
||||
|
|
|
|||
|
|
@ -3675,6 +3675,9 @@
|
|||
"items": {
|
||||
"$ref": "#/$defs/OpenConnectFormEntryOptions"
|
||||
}
|
||||
},
|
||||
"on_demand": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -4080,6 +4083,9 @@
|
|||
},
|
||||
"udp_timeout": {
|
||||
"$ref": "#/$defs/UDPTimeout"
|
||||
},
|
||||
"on_demand": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -4546,6 +4552,9 @@
|
|||
},
|
||||
"taildrop_directory": {
|
||||
"type": "string"
|
||||
},
|
||||
"on_demand": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -4629,6 +4638,9 @@
|
|||
"workers": {
|
||||
"type": "integer"
|
||||
},
|
||||
"on_demand": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"detour": {
|
||||
"type": "string",
|
||||
"x-tag-reference": "outbound"
|
||||
|
|
|
|||
6
go.mod
6
go.mod
|
|
@ -45,11 +45,11 @@ require (
|
|||
github.com/sagernet/nftables v0.3.0-mod.4
|
||||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.9
|
||||
github.com/sagernet/sing v0.9.7-0.20260929150544-6f21f2425a95
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260902142641-43bb1cbea74e
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be
|
||||
github.com/sagernet/sing-cloudflared v0.1.4-0.20261002084126-c1255ae368f2
|
||||
github.com/sagernet/sing-mux v0.3.10-0.20260929204512-caf09fe32475
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151225-cbd68c45de58
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-7c3045dbddce
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151226-29757281a247
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-f330676d6d4a
|
||||
github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9-0.20260929204512-65740e0f0e3e
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.2-0.20260929152114-a69d1086332b
|
||||
|
|
|
|||
12
go.sum
12
go.sum
|
|
@ -318,16 +318,16 @@ github.com/sagernet/quic-go v0.61.0-sing-box-mod.9 h1:Lojx77uC63TxZO38Z1OSo6aOoB
|
|||
github.com/sagernet/quic-go v0.61.0-sing-box-mod.9/go.mod h1:hmLC8GJPp+BrpBgrgJnacvI4fZGIhasuy1ROJJ8GR4E=
|
||||
github.com/sagernet/sing v0.9.7-0.20260929150544-6f21f2425a95 h1:taM2SOI5HK/Ubpi3FO0Bc7bKHsFVdGe4YS/jjXD4kAM=
|
||||
github.com/sagernet/sing v0.9.7-0.20260929150544-6f21f2425a95/go.mod h1:K3Owt3xPhHugvlnlPPxZJ/exXdaJfEPOTNorGk4AXjo=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260902142641-43bb1cbea74e h1:le983HDWe+D4LnnexDf3WXgHGYV8mxQ41n0cQL39cro=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260902142641-43bb1cbea74e/go.mod h1:VRKLnj0ObEsN228h+zuzf91p+6sjtYE8SBaLPVsBy5o=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be h1:0uMXu8kcJU0XLc2vfjGH6q6khmECCc5WYpD9dqlu1Xg=
|
||||
github.com/sagernet/sing-anytls v0.0.0-20260904135308-cec2d74334be/go.mod h1:VRKLnj0ObEsN228h+zuzf91p+6sjtYE8SBaLPVsBy5o=
|
||||
github.com/sagernet/sing-cloudflared v0.1.4-0.20261002084126-c1255ae368f2 h1:+AfbAXGEV8tezIYHeShzwHm7IMUE5kYGVFLo0ly7W24=
|
||||
github.com/sagernet/sing-cloudflared v0.1.4-0.20261002084126-c1255ae368f2/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
|
||||
github.com/sagernet/sing-mux v0.3.10-0.20260929204512-caf09fe32475 h1:aG0DqHEOjax7DyfE1GTAXZotOg3QGVCz3/8rCSua2kg=
|
||||
github.com/sagernet/sing-mux v0.3.10-0.20260929204512-caf09fe32475/go.mod h1:jricpwCOndDsTEM1TM3piJvhL/2Mm7OhLkm+JadJJ1Q=
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151225-cbd68c45de58 h1:WOPVlvbw3zGl4IzpRcfqB901blgDKuiplVrpptf/TOY=
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151225-cbd68c45de58/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-7c3045dbddce h1:uB2DkIJLZ9waR/ZLBO9IF/ws5BWg0FEegLi90ibhX5I=
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-7c3045dbddce/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151226-29757281a247 h1:Ui4XtxNgQiEquK+ch4dYhthKsVlGQpdCdKIpOuJVyog=
|
||||
github.com/sagernet/sing-openconnect v0.1.1-0.20260929151226-29757281a247/go.mod h1:4AKZLVcvY3r54UaK2Gbnm7aN8pOwdLz+y4EP0QFZ5Eg=
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-f330676d6d4a h1:Q16Twlwk9fssoCSUTDo5WqPg0EZHG9JSV9SC3zRy9M0=
|
||||
github.com/sagernet/sing-openvpn v0.1.1-0.20260929151220-f330676d6d4a/go.mod h1:PWX7WygD8jpwfqfaGNySXpJYTn0SOwjBI1BKHHC2+Bw=
|
||||
github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b h1:2Jx/URJAPt32MEbLmAR5nzH87tO6NQZLRfRFvFqBWuk=
|
||||
github.com/sagernet/sing-quic v0.7.2-0.20261002084117-75c3ac4fa12b/go.mod h1:rJFeVxbEej2iuyeCJ3drKW+p5rOgtLIW3XhZ6ty7Fig=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9-0.20260929204512-65740e0f0e3e h1:LKgE/uuqp/D2kgxYViOz8vf4slaCUfpFywXjtuwD5gU=
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ type OpenConnectEndpointOptions struct {
|
|||
AllowInsecureCrypto bool `json:"allow_insecure_crypto,omitempty"`
|
||||
TLS OpenConnectTLSOptions `json:"tls,omitempty"`
|
||||
FormEntries []OpenConnectFormEntryOptions `json:"form_entries,omitempty"`
|
||||
OnDemand bool `json:"on_demand,omitempty"`
|
||||
}
|
||||
|
||||
type OpenConnectTokenOptions struct {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ type OpenVPNClientEndpointOptions struct {
|
|||
HandshakeWindow badoption.Duration `json:"handshake_window,omitempty"`
|
||||
ExplicitExitNotify uint32 `json:"explicit_exit_notify,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
OnDemand bool `json:"on_demand,omitempty"`
|
||||
}
|
||||
|
||||
type OpenVPNServerEndpointOptions struct {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ type TailscaleEndpointOptions struct {
|
|||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
SSHServer *TailscaleSSHServerOptions `json:"ssh_server,omitempty"`
|
||||
TaildropDirectory string `json:"taildrop_directory,omitempty"`
|
||||
OnDemand bool `json:"on_demand,omitempty"`
|
||||
}
|
||||
|
||||
type _TailscaleSSHServerOptions struct {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type WireGuardEndpointOptions struct {
|
|||
UDPFiltering UDPNATBehavior `json:"udp_filtering,omitempty"`
|
||||
UDPNATMax uint32 `json:"udp_nat_max,omitempty"`
|
||||
Workers int `json:"workers,omitempty"`
|
||||
OnDemand bool `json:"on_demand,omitempty"`
|
||||
DialerOptions
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ func RegisterOutbound(registry *outbound.Registry) {
|
|||
var (
|
||||
_ adapter.OutboundWithMultiplex = (*Outbound)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Outbound)(nil)
|
||||
_ adapter.IdleConnectionCloser = (*Outbound)(nil)
|
||||
_ adapter.IdleConnectionKeeper = (*Outbound)(nil)
|
||||
)
|
||||
|
||||
type Outbound struct {
|
||||
|
|
@ -122,6 +122,10 @@ func (h *Outbound) InterfaceUpdated(ctx context.Context) {
|
|||
h.client.Reset()
|
||||
}
|
||||
|
||||
func (h *Outbound) SetKeepIdleConnections(keep bool) {
|
||||
h.client.SetKeepIdleConnections(keep)
|
||||
}
|
||||
|
||||
func (h *Outbound) CloseIdleConnections() {
|
||||
h.client.CloseIdleConnections()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ var (
|
|||
_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
|
||||
_ adapter.FlowOutbound = (*Endpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Endpoint)(nil)
|
||||
_ adapter.OnDemandEndpoint = (*Endpoint)(nil)
|
||||
_ dialer.PacketDialerWithDestination = (*Endpoint)(nil)
|
||||
_ tun.Port = (*Endpoint)(nil)
|
||||
)
|
||||
|
|
@ -48,6 +49,7 @@ type Endpoint struct {
|
|||
dnsRouter adapter.DNSRouter
|
||||
client *openconnect.Client
|
||||
device openconnecttransport.Device
|
||||
onDemand bool
|
||||
server string
|
||||
flavor string
|
||||
stateAccess sync.Mutex
|
||||
|
|
@ -111,6 +113,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||
cancelLoop: cancelLoop,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
statusUpdated: make(chan struct{}),
|
||||
onDemand: options.OnDemand,
|
||||
}
|
||||
openConnectEndpoint.state.Store(new(clientState))
|
||||
success := false
|
||||
|
|
@ -490,6 +493,49 @@ func (e *Endpoint) InterfaceUpdated(ctx context.Context) {
|
|||
e.client.RestartSession()
|
||||
}
|
||||
|
||||
func (e *Endpoint) OnDemand() bool {
|
||||
return e.onDemand
|
||||
}
|
||||
|
||||
func (e *Endpoint) SetKeepIdleConnections(keep bool) {
|
||||
if !keep {
|
||||
e.client.Suspend()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Endpoint) waitReady(ctx context.Context) error {
|
||||
if !e.onDemand {
|
||||
if !e.ready() || !e.client.Ready() {
|
||||
return E.New("endpoint is not ready yet")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
e.client.Resume()
|
||||
waitCtx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
err := e.client.WaitReady(waitCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
e.statusAccess.Lock()
|
||||
statusUpdated := e.statusUpdated
|
||||
terminalError := e.terminalError
|
||||
e.statusAccess.Unlock()
|
||||
if terminalError != "" {
|
||||
return E.New(terminalError)
|
||||
}
|
||||
if e.ready() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-waitCtx.Done():
|
||||
return waitCtx.Err()
|
||||
case <-statusUpdated:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Endpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
|
||||
return adapter.PreMatchFlow
|
||||
}
|
||||
|
|
@ -524,6 +570,9 @@ func (e *Endpoint) ready() bool {
|
|||
}
|
||||
|
||||
func (e *Endpoint) WritePackets(packets [][]byte) error {
|
||||
if e.onDemand {
|
||||
e.client.Resume()
|
||||
}
|
||||
if !e.ready() {
|
||||
return E.New("endpoint is not ready yet")
|
||||
}
|
||||
|
|
@ -535,6 +584,9 @@ func (e *Endpoint) WritePackets(packets [][]byte) error {
|
|||
}
|
||||
|
||||
func (e *Endpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
|
||||
if e.onDemand {
|
||||
e.client.Resume()
|
||||
}
|
||||
if !e.ready() {
|
||||
buf.ReleaseMulti(packetBuffers)
|
||||
return nil
|
||||
|
|
@ -561,8 +613,9 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination
|
|||
case N.NetworkUDP:
|
||||
e.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
}
|
||||
if !e.ready() || !e.client.Ready() {
|
||||
return nil, E.New("endpoint is not ready yet")
|
||||
readyErr := e.waitReady(ctx)
|
||||
if readyErr != nil {
|
||||
return nil, readyErr
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
|
|
@ -579,8 +632,9 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination
|
|||
|
||||
func (e *Endpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
|
||||
e.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
if !e.ready() || !e.client.Ready() {
|
||||
return nil, netip.Addr{}, E.New("endpoint is not ready yet")
|
||||
readyErr := e.waitReady(ctx)
|
||||
if readyErr != nil {
|
||||
return nil, netip.Addr{}, readyErr
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := e.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ var (
|
|||
_ adapter.OutboundWithPreferredRoutes = (*ClientEndpoint)(nil)
|
||||
_ adapter.FlowOutbound = (*ClientEndpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*ClientEndpoint)(nil)
|
||||
_ adapter.OnDemandEndpoint = (*ClientEndpoint)(nil)
|
||||
_ dialer.PacketDialerWithDestination = (*ClientEndpoint)(nil)
|
||||
_ tun.Port = (*ClientEndpoint)(nil)
|
||||
)
|
||||
|
|
@ -50,6 +51,7 @@ type ClientEndpoint struct {
|
|||
queryOptions adapter.DNSQueryOptions
|
||||
client *ovpn.Client
|
||||
device ovpntransport.Device
|
||||
onDemand bool
|
||||
stateAccess sync.Mutex
|
||||
state atomic.Pointer[clientState]
|
||||
dnsTransport *DNSTransport
|
||||
|
|
@ -83,6 +85,7 @@ func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.Co
|
|||
cancelLoop: cancelLoop,
|
||||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
statusUpdated: make(chan struct{}),
|
||||
onDemand: options.OnDemand,
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
|
|
@ -666,6 +669,49 @@ func (c *ClientEndpoint) InterfaceUpdated(ctx context.Context) {
|
|||
c.client.RestartSession()
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) OnDemand() bool {
|
||||
return c.onDemand
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) SetKeepIdleConnections(keep bool) {
|
||||
if !keep {
|
||||
c.client.Suspend()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) waitReady(ctx context.Context) error {
|
||||
if !c.onDemand {
|
||||
if !c.ready() || !c.client.Ready() {
|
||||
return E.New("endpoint is not ready yet")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
c.client.Resume()
|
||||
waitCtx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||
defer cancel()
|
||||
err := c.client.WaitReady(waitCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
c.statusAccess.Lock()
|
||||
statusUpdated := c.statusUpdated
|
||||
terminalError := c.terminalError
|
||||
c.statusAccess.Unlock()
|
||||
if terminalError != "" {
|
||||
return E.New(terminalError)
|
||||
}
|
||||
if c.ready() {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-waitCtx.Done():
|
||||
return waitCtx.Err()
|
||||
case <-statusUpdated:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClientEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
|
||||
return adapter.PreMatchFlow
|
||||
}
|
||||
|
|
@ -700,6 +746,9 @@ func (c *ClientEndpoint) ready() bool {
|
|||
}
|
||||
|
||||
func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
|
||||
if c.onDemand {
|
||||
c.client.Resume()
|
||||
}
|
||||
state := c.state.Load()
|
||||
if !state.started || !state.tunnelConfigured {
|
||||
return E.New("endpoint is not ready yet")
|
||||
|
|
@ -728,6 +777,9 @@ func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
|
|||
}
|
||||
|
||||
func (c *ClientEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
|
||||
if c.onDemand {
|
||||
c.client.Resume()
|
||||
}
|
||||
state := c.state.Load()
|
||||
if !state.started || !state.tunnelConfigured {
|
||||
buf.ReleaseMulti(packetBuffers)
|
||||
|
|
@ -769,8 +821,9 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin
|
|||
case N.NetworkUDP:
|
||||
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
}
|
||||
if !c.ready() || !c.client.Ready() {
|
||||
return nil, E.New("endpoint is not ready yet")
|
||||
readyErr := c.waitReady(ctx)
|
||||
if readyErr != nil {
|
||||
return nil, readyErr
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
|
|
@ -787,8 +840,9 @@ func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destin
|
|||
|
||||
func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
|
||||
c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
|
||||
if !c.ready() || !c.client.Ready() {
|
||||
return nil, netip.Addr{}, E.New("endpoint is not ready yet")
|
||||
readyErr := c.waitReady(ctx)
|
||||
if readyErr != nil {
|
||||
return nil, netip.Addr{}, readyErr
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import (
|
|||
tailscaleroot "github.com/sagernet/tailscale"
|
||||
_ "github.com/sagernet/tailscale/feature/relayserver"
|
||||
"github.com/sagernet/tailscale/ipn"
|
||||
"github.com/sagernet/tailscale/ipn/ipnlocal"
|
||||
tsDNS "github.com/sagernet/tailscale/net/dns"
|
||||
"github.com/sagernet/tailscale/net/netmon"
|
||||
"github.com/sagernet/tailscale/net/netns"
|
||||
|
|
@ -68,6 +69,7 @@ var (
|
|||
_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Endpoint)(nil)
|
||||
_ adapter.Referrer = (*Endpoint)(nil)
|
||||
_ adapter.OnDemandEndpoint = (*Endpoint)(nil)
|
||||
_ dialer.PacketDialerWithDestination = (*Endpoint)(nil)
|
||||
_ tun.Port = (*Endpoint)(nil)
|
||||
)
|
||||
|
|
@ -120,6 +122,13 @@ type Endpoint struct {
|
|||
sshServerInstance *tailssh.Server
|
||||
sshServerOptions *option.TailscaleSSHServerOptions
|
||||
taildrop *taildropManager
|
||||
localBackend atomic.Pointer[ipnlocal.LocalBackend]
|
||||
onDemand bool
|
||||
suspendAccess sync.Mutex
|
||||
idleRequested atomic.Bool
|
||||
resumeDone chan struct{}
|
||||
resumePending atomic.Bool
|
||||
suspended atomic.Bool
|
||||
|
||||
systemInterface bool
|
||||
systemInterfaceName string
|
||||
|
|
@ -245,6 +254,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||
systemInterfaceName: options.SystemInterfaceName,
|
||||
systemInterfaceMTU: options.SystemInterfaceMTU,
|
||||
keyAuth: options.AuthKey != "",
|
||||
onDemand: options.OnDemand,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +449,11 @@ func (t *Endpoint) postStart(scope *adapter.Scope) error {
|
|||
return nil
|
||||
})
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
t.localBackend.Store(localBackend)
|
||||
scope.Add(func() error {
|
||||
t.localBackend.Store(nil)
|
||||
return nil
|
||||
})
|
||||
scope.Add(func() error {
|
||||
t.taildrop.close()
|
||||
return nil
|
||||
|
|
@ -598,6 +613,7 @@ func (t *Endpoint) watchState(ctx context.Context) {
|
|||
if exitNodePending {
|
||||
tryApplyExitNode()
|
||||
}
|
||||
t.suspendIfRequested()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
|
@ -752,6 +768,132 @@ func (t *Endpoint) InterfaceUpdated(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *Endpoint) OnDemand() bool {
|
||||
return t.onDemand
|
||||
}
|
||||
|
||||
func (t *Endpoint) SetKeepIdleConnections(keep bool) {
|
||||
t.idleRequested.Store(!keep)
|
||||
if !keep {
|
||||
t.suspendAccess.Lock()
|
||||
t.suspendLocked()
|
||||
t.suspendAccess.Unlock()
|
||||
return
|
||||
}
|
||||
if t.systemInterface {
|
||||
t.requestResume()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Endpoint) requestResume() {
|
||||
if !t.suspended.Load() || !t.resumePending.CompareAndSwap(false, true) {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer t.resumePending.Store(false)
|
||||
err := t.resume(t.ctx)
|
||||
if err != nil {
|
||||
t.logger.Error(E.Cause(err, "resume"))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Endpoint) suspendIfRequested() {
|
||||
if !t.idleRequested.Load() {
|
||||
return
|
||||
}
|
||||
t.suspendAccess.Lock()
|
||||
if t.idleRequested.Load() {
|
||||
t.suspendLocked()
|
||||
}
|
||||
t.suspendAccess.Unlock()
|
||||
}
|
||||
|
||||
func (t *Endpoint) suspendLocked() {
|
||||
if t.suspended.Load() || t.resumeDone != nil {
|
||||
return
|
||||
}
|
||||
localBackend := t.localBackend.Load()
|
||||
if localBackend == nil || localBackend.State() != ipn.Running {
|
||||
return
|
||||
}
|
||||
_, err := localBackend.EditPrefs(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
WantRunningSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.logger.Error(E.Cause(err, "suspend"))
|
||||
return
|
||||
}
|
||||
t.suspended.Store(true)
|
||||
}
|
||||
|
||||
func (t *Endpoint) resume(ctx context.Context) error {
|
||||
t.idleRequested.Store(false)
|
||||
if !t.suspended.Load() {
|
||||
return nil
|
||||
}
|
||||
t.suspendAccess.Lock()
|
||||
if !t.suspended.Load() {
|
||||
t.suspendAccess.Unlock()
|
||||
return nil
|
||||
}
|
||||
resumeDone := t.resumeDone
|
||||
if resumeDone == nil {
|
||||
localBackend := t.localBackend.Load()
|
||||
if localBackend == nil {
|
||||
t.suspendAccess.Unlock()
|
||||
return E.New("Tailscale is not ready yet")
|
||||
}
|
||||
_, err := localBackend.EditPrefs(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
WantRunningSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.suspendAccess.Unlock()
|
||||
return err
|
||||
}
|
||||
resumeDone = make(chan struct{})
|
||||
t.resumeDone = resumeDone
|
||||
go t.awaitRunning(localBackend, resumeDone)
|
||||
}
|
||||
t.suspendAccess.Unlock()
|
||||
select {
|
||||
case <-resumeDone:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if t.suspended.Load() {
|
||||
return E.New("Tailscale backend is not running")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Endpoint) awaitRunning(localBackend *ipnlocal.LocalBackend, resumeDone chan struct{}) {
|
||||
running := localBackend.State() == ipn.Running
|
||||
if !running {
|
||||
watchCtx, cancel := context.WithTimeout(t.ctx, C.TCPTimeout)
|
||||
localBackend.WatchNotifications(watchCtx, ipn.NotifyInitialState, nil, func(notify *ipn.Notify) bool {
|
||||
if notify.State != nil && *notify.State == ipn.Running {
|
||||
running = true
|
||||
return false
|
||||
}
|
||||
return notify.ErrMessage == nil
|
||||
})
|
||||
cancel()
|
||||
}
|
||||
t.suspendAccess.Lock()
|
||||
t.resumeDone = nil
|
||||
if running {
|
||||
t.suspended.Store(false)
|
||||
if t.idleRequested.Load() {
|
||||
t.suspendLocked()
|
||||
}
|
||||
}
|
||||
t.suspendAccess.Unlock()
|
||||
close(resumeDone)
|
||||
}
|
||||
|
||||
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
|
|
@ -762,6 +904,10 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination
|
|||
if !t.started.Load() {
|
||||
return nil, E.New("Tailscale is not ready yet")
|
||||
}
|
||||
resumeErr := t.resume(ctx)
|
||||
if resumeErr != nil {
|
||||
return nil, resumeErr
|
||||
}
|
||||
if destination.IsDomain() {
|
||||
destinationAddresses, err := t.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
|
||||
if err != nil {
|
||||
|
|
@ -821,6 +967,10 @@ func (t *Endpoint) listenPacketWithAddress(ctx context.Context, destination M.So
|
|||
if !t.started.Load() {
|
||||
return nil, E.New("Tailscale is not ready yet")
|
||||
}
|
||||
resumeErr := t.resume(ctx)
|
||||
if resumeErr != nil {
|
||||
return nil, resumeErr
|
||||
}
|
||||
if t.systemDialer != nil {
|
||||
return t.systemDialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ func (t *Endpoint) WritePackets(packets [][]byte) error {
|
|||
if !t.started.Load() {
|
||||
return E.New("Tailscale is not ready yet")
|
||||
}
|
||||
t.requestResume()
|
||||
unmatched, err := t.wgEngine.InputPackets(packets)
|
||||
if err != nil || len(unmatched) == 0 {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import (
|
|||
var (
|
||||
_ adapter.OutboundWithPreferredRoutes = (*Endpoint)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*Endpoint)(nil)
|
||||
_ adapter.OnDemandEndpoint = (*Endpoint)(nil)
|
||||
_ dialer.PacketDialerWithDestination = (*Endpoint)(nil)
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +45,7 @@ type Endpoint struct {
|
|||
logger logger.ContextLogger
|
||||
localAddresses []netip.Prefix
|
||||
endpoint *wireguard.Endpoint
|
||||
onDemand bool
|
||||
bindAccess sync.Mutex
|
||||
started atomic.Bool
|
||||
}
|
||||
|
|
@ -56,6 +58,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||
dnsRouter: service.FromContext[adapter.DNSRouter](ctx),
|
||||
logger: logger,
|
||||
localAddresses: options.Address,
|
||||
onDemand: options.OnDemand,
|
||||
}
|
||||
if options.Detour != "" && options.ListenPort != 0 {
|
||||
return nil, E.New("`listen_port` is conflict with `detour`")
|
||||
|
|
@ -163,6 +166,14 @@ func (w *Endpoint) InterfaceUpdated(ctx context.Context) {
|
|||
go w.updateBind(ctx)
|
||||
}
|
||||
|
||||
func (w *Endpoint) OnDemand() bool {
|
||||
return w.onDemand
|
||||
}
|
||||
|
||||
func (w *Endpoint) SetKeepIdleConnections(keep bool) {
|
||||
w.endpoint.SetIdle(!keep)
|
||||
}
|
||||
|
||||
func (w *Endpoint) updateBind(ctx context.Context) {
|
||||
w.bindAccess.Lock()
|
||||
defer w.bindAccess.Unlock()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package route
|
|||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
var _ adapter.LifecycleService = (*ReferenceManager)(nil)
|
||||
|
|
@ -23,6 +25,9 @@ type ReferenceManager struct {
|
|||
staticOutbounds []string
|
||||
staticTransports []string
|
||||
subscriber *observable.Subscriber[struct{}]
|
||||
pauseManager pause.Manager
|
||||
devicePaused atomic.Bool
|
||||
idleFlushed bool
|
||||
keepIdle map[any]bool
|
||||
unreferencedTransports map[string]bool
|
||||
}
|
||||
|
|
@ -58,6 +63,7 @@ func NewReferenceManager(ctx context.Context, logger log.ContextLogger, options
|
|||
dnsRules: dnsRules,
|
||||
staticOutbounds: staticOutbounds,
|
||||
staticTransports: staticTransports,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,8 +97,28 @@ func (m *ReferenceManager) Start(stage adapter.StartStage, scope *adapter.Scope)
|
|||
if clashMode != nil {
|
||||
clashMode.AddUpdateHook(m.subscriber)
|
||||
}
|
||||
if m.pauseManager != nil {
|
||||
m.devicePaused.Store(m.pauseManager.IsDevicePaused())
|
||||
}
|
||||
m.update()
|
||||
go m.loop()
|
||||
if m.pauseManager != nil {
|
||||
pauseCallback := m.pauseManager.RegisterCallback(func(event int) {
|
||||
switch event {
|
||||
case pause.EventDevicePaused:
|
||||
m.devicePaused.Store(true)
|
||||
case pause.EventDeviceWake:
|
||||
m.devicePaused.Store(false)
|
||||
default:
|
||||
return
|
||||
}
|
||||
m.subscriber.Emit(struct{}{})
|
||||
})
|
||||
scope.Add(func() error {
|
||||
m.pauseManager.UnregisterCallback(pauseCallback)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -137,7 +163,13 @@ func (m *ReferenceManager) update() {
|
|||
outboundQueue = append(outboundQueue, defaultOutbound.Tag())
|
||||
}
|
||||
}
|
||||
var onDemandEndpoints []adapter.OnDemandEndpoint
|
||||
for _, endpoint := range endpointManager.Endpoints() {
|
||||
onDemandEndpoint, isOnDemandEndpoint := endpoint.(adapter.OnDemandEndpoint)
|
||||
if isOnDemandEndpoint && onDemandEndpoint.OnDemand() {
|
||||
onDemandEndpoints = append(onDemandEndpoints, onDemandEndpoint)
|
||||
continue
|
||||
}
|
||||
outboundQueue = append(outboundQueue, endpoint.Tag())
|
||||
}
|
||||
for _, inbound := range inboundManager.Inbounds() {
|
||||
|
|
@ -195,6 +227,7 @@ func (m *ReferenceManager) update() {
|
|||
}
|
||||
}
|
||||
|
||||
devicePaused := m.devicePaused.Load()
|
||||
keepIdle := make(map[any]bool)
|
||||
for _, outbound := range outboundManager.Outbounds() {
|
||||
keeper, isKeeper := outbound.(adapter.IdleConnectionKeeper)
|
||||
|
|
@ -211,6 +244,18 @@ func (m *ReferenceManager) update() {
|
|||
keep: referencedOutbounds[outbound.Tag()],
|
||||
})
|
||||
}
|
||||
for _, endpoint := range onDemandEndpoints {
|
||||
m.applyKeepIdle(keepIdle, idleTarget{
|
||||
value: endpoint,
|
||||
keeper: endpoint,
|
||||
kind: "endpoint/",
|
||||
action: "suspending",
|
||||
typeName: endpoint.Type(),
|
||||
tag: endpoint.Tag(),
|
||||
keep: referencedOutbounds[endpoint.Tag()] && !devicePaused,
|
||||
devicePaused: devicePaused,
|
||||
})
|
||||
}
|
||||
unreferencedTransports := make(map[string]bool)
|
||||
for _, transport := range transportManager.Transports() {
|
||||
tag := transport.Tag()
|
||||
|
|
@ -239,6 +284,10 @@ func (m *ReferenceManager) update() {
|
|||
}
|
||||
m.keepIdle = keepIdle
|
||||
m.unreferencedTransports = unreferencedTransports
|
||||
if devicePaused && !m.idleFlushed {
|
||||
m.CloseIdleConnections()
|
||||
}
|
||||
m.idleFlushed = devicePaused
|
||||
}
|
||||
|
||||
type idleKeeper interface {
|
||||
|
|
@ -246,13 +295,14 @@ type idleKeeper interface {
|
|||
}
|
||||
|
||||
type idleTarget struct {
|
||||
value any
|
||||
keeper idleKeeper
|
||||
kind string
|
||||
action string
|
||||
typeName string
|
||||
tag string
|
||||
keep bool
|
||||
value any
|
||||
keeper idleKeeper
|
||||
kind string
|
||||
action string
|
||||
typeName string
|
||||
tag string
|
||||
keep bool
|
||||
devicePaused bool
|
||||
}
|
||||
|
||||
func (m *ReferenceManager) applyKeepIdle(keepIdle map[any]bool, target idleTarget) {
|
||||
|
|
@ -262,7 +312,11 @@ func (m *ReferenceManager) applyKeepIdle(keepIdle map[any]bool, target idleTarge
|
|||
return
|
||||
}
|
||||
if !target.keep {
|
||||
m.logger.Debug(target.kind, target.typeName, "[", target.tag, "] is unreferenced, ", target.action)
|
||||
if target.devicePaused {
|
||||
m.logger.Debug(target.kind, target.typeName, "[", target.tag, "] device paused, ", target.action)
|
||||
} else {
|
||||
m.logger.Debug(target.kind, target.typeName, "[", target.tag, "] is unreferenced, ", target.action)
|
||||
}
|
||||
}
|
||||
target.keeper.SetKeepIdleConnections(target.keep)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import (
|
|||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/service/powerreport"
|
||||
|
|
@ -33,11 +35,14 @@ type Endpoint struct {
|
|||
allowedAddress []netip.Prefix
|
||||
tunDevice Device
|
||||
returnDevice *returnDeviceWrapper
|
||||
device *device.Device
|
||||
device atomic.Pointer[device.Device]
|
||||
allowedIPs *device.AllowedIPs
|
||||
egressPool *tun.UDPEgressPool
|
||||
pause pause.Manager
|
||||
pauseCallback *list.Element[pause.Callback]
|
||||
stateAccess sync.Mutex
|
||||
suspended atomic.Bool
|
||||
networkPaused bool
|
||||
}
|
||||
|
||||
func NewEndpoint(options EndpointOptions) (*Endpoint, error) {
|
||||
|
|
@ -253,7 +258,7 @@ func (e *Endpoint) Start(postStart bool) error {
|
|||
wgDevice.Close()
|
||||
return E.Cause(err, "setup wireguard: \n", ipcConf.String())
|
||||
}
|
||||
e.device = wgDevice
|
||||
e.device.Store(wgDevice)
|
||||
e.pause = service.FromContext[pause.Manager](e.options.Context)
|
||||
if e.pause != nil {
|
||||
e.pauseCallback = e.pause.RegisterCallback(e.onPauseUpdated)
|
||||
|
|
@ -266,6 +271,7 @@ func (e *Endpoint) DialContext(ctx context.Context, network string, destination
|
|||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
e.resume()
|
||||
return e.tunDevice.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
|
|
@ -273,9 +279,51 @@ func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
|||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
e.resume()
|
||||
return e.tunDevice.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (e *Endpoint) SetIdle(idle bool) {
|
||||
e.stateAccess.Lock()
|
||||
defer e.stateAccess.Unlock()
|
||||
wgDevice := e.device.Load()
|
||||
if wgDevice == nil {
|
||||
return
|
||||
}
|
||||
if idle {
|
||||
if e.suspended.Load() {
|
||||
return
|
||||
}
|
||||
e.suspended.Store(true)
|
||||
wgDevice.Down()
|
||||
} else if e.options.System {
|
||||
e.resumeLocked(wgDevice)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Endpoint) resume() {
|
||||
if !e.suspended.Load() {
|
||||
return
|
||||
}
|
||||
e.stateAccess.Lock()
|
||||
defer e.stateAccess.Unlock()
|
||||
wgDevice := e.device.Load()
|
||||
if wgDevice == nil {
|
||||
return
|
||||
}
|
||||
e.resumeLocked(wgDevice)
|
||||
}
|
||||
|
||||
func (e *Endpoint) resumeLocked(wgDevice *device.Device) {
|
||||
if !e.suspended.Load() {
|
||||
return
|
||||
}
|
||||
e.suspended.Store(false)
|
||||
if !e.networkPaused {
|
||||
wgDevice.Up()
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Endpoint) Close() error {
|
||||
if e.pauseCallback != nil {
|
||||
e.pause.UnregisterCallback(e.pauseCallback)
|
||||
|
|
@ -285,10 +333,14 @@ func (e *Endpoint) Close() error {
|
|||
e.egressPool.Close()
|
||||
e.egressPool = nil
|
||||
}
|
||||
if e.device != nil {
|
||||
e.device.Down()
|
||||
e.device.Close()
|
||||
e.device = nil
|
||||
e.stateAccess.Lock()
|
||||
wgDevice := e.device.Swap(nil)
|
||||
if wgDevice != nil {
|
||||
wgDevice.Down()
|
||||
wgDevice.Close()
|
||||
}
|
||||
e.stateAccess.Unlock()
|
||||
if wgDevice != nil {
|
||||
return nil
|
||||
}
|
||||
return e.tunDevice.Close()
|
||||
|
|
@ -302,18 +354,29 @@ func (e *Endpoint) Lookup(address netip.Addr) *device.Peer {
|
|||
}
|
||||
|
||||
func (e *Endpoint) BindUpdate() error {
|
||||
if e.device == nil {
|
||||
wgDevice := e.device.Load()
|
||||
if wgDevice == nil {
|
||||
return nil
|
||||
}
|
||||
return e.device.BindUpdate()
|
||||
return wgDevice.BindUpdate()
|
||||
}
|
||||
|
||||
func (e *Endpoint) onPauseUpdated(event int) {
|
||||
e.stateAccess.Lock()
|
||||
defer e.stateAccess.Unlock()
|
||||
wgDevice := e.device.Load()
|
||||
if wgDevice == nil {
|
||||
return
|
||||
}
|
||||
switch event {
|
||||
case pause.EventNetworkPause:
|
||||
e.device.Down()
|
||||
e.networkPaused = true
|
||||
wgDevice.Down()
|
||||
case pause.EventNetworkWake:
|
||||
e.device.Up()
|
||||
e.networkPaused = false
|
||||
if !e.suspended.Load() {
|
||||
wgDevice.Up()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,11 @@ func (e *Endpoint) PortMTU() uint32 {
|
|||
}
|
||||
|
||||
func (e *Endpoint) WritePackets(packets [][]byte) error {
|
||||
wgDevice := e.device
|
||||
wgDevice := e.device.Load()
|
||||
if wgDevice == nil {
|
||||
return E.New("WireGuard device is not ready")
|
||||
}
|
||||
e.resume()
|
||||
packetRefs := make([]*device.InputPacketRef, 0, len(packets))
|
||||
refs := make([]device.InputPacketRef, len(packets))
|
||||
packetSlices := make([][]byte, len(packets))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue