44 "context"
55 "net/netip"
66 "os"
7+ "sync"
78 "syscall"
89
910 box "github.com/sagernet/sing-box"
@@ -16,10 +17,13 @@ import (
1617 "github.com/sagernet/sing/common/logger"
1718 "github.com/sagernet/sing/common/x/list"
1819 "github.com/sagernet/sing/service"
20+ "github.com/sagernet/sing/service/pause"
1921)
2022
2123type singBoxRunner struct {
22- box * box.Box
24+ box * box.Box
25+ pauseManager pause.Manager
26+ platform * singBoxPlatform
2327}
2428
2529func (s * singBoxRunner ) stop () error {
@@ -29,17 +33,46 @@ func (s *singBoxRunner) stop() error {
2933 return nil
3034}
3135
36+ func (s * singBoxRunner ) suspend () {
37+ if s .pauseManager != nil && ! s .pauseManager .IsDevicePaused () {
38+ s .pauseManager .DevicePause ()
39+ }
40+ }
41+
42+ func (s * singBoxRunner ) resume () {
43+ if s .pauseManager != nil && s .pauseManager .IsDevicePaused () {
44+ s .pauseManager .DeviceWake ()
45+ }
46+ }
47+
48+ func (s * singBoxRunner ) updateDefaultInterface (name string , index int32 , isExpensive , isConstrained bool ) {
49+ if s .platform == nil || s .platform .monitor == nil {
50+ return
51+ }
52+ var nm adapter.NetworkManager
53+ if s .box != nil {
54+ nm = s .box .Network ()
55+ }
56+ s .platform .monitor .updateDefault (nm , name , index , isExpensive , isConstrained )
57+ }
58+
3259func startSingBox (configContent string , tunFD , _ int ) (coreRunner , error ) {
3360 // include.Context attaches the built-in inbound/outbound/endpoint/
3461 // DNS-transport/service registries to the context. Without it
3562 // box.New cannot resolve types declared in the JSON (socks,
3663 // direct, vmess, …) and start fails immediately.
3764 ctx := include .Context (context .Background ())
3865
39- // sing-box's tun inbound has no FileDescriptor field in its JSON
40- // schema — the FD is plumbed via an adapter.PlatformInterface
41- // whose OpenInterface is invoked while wiring the inbound.
42- pi := & singBoxPlatform {tunFD : tunFD }
66+ // box.New runs pause.WithDefaultManager on its own copy of the
67+ // context, so the manager it installs is unreachable from out here.
68+ // Install one on our ctx up front and box.New will reuse it
69+ // (WithDefaultManager early-returns when a manager is already
70+ // registered), which lets us hand DevicePause/DeviceWake to the
71+ // NE's sleep/wake callbacks.
72+ ctx = pause .WithDefaultManager (ctx )
73+ pauseManager := service.FromContext [pause.Manager ](ctx )
74+
75+ pi := newSingBoxPlatform (tunFD )
4376 ctx = service .ContextWith [adapter.PlatformInterface ](ctx , pi )
4477
4578 options , err := json .UnmarshalExtendedContext [option.Options ](ctx , []byte (configContent ))
@@ -57,7 +90,7 @@ func startSingBox(configContent string, tunFD, _ int) (coreRunner, error) {
5790 _ = b .Close ()
5891 return nil , err
5992 }
60- return & singBoxRunner {box : b }, nil
93+ return & singBoxRunner {box : b , pauseManager : pauseManager , platform : pi }, nil
6194}
6295
6396// singBoxPlatform is the minimal adapter.PlatformInterface needed
@@ -70,6 +103,14 @@ func startSingBox(configContent string, tunFD, _ int) (coreRunner, error) {
70103type singBoxPlatform struct {
71104 tunFD int
72105 myAddresses []netip.Addr
106+ monitor * singBoxInterfaceMonitor
107+ }
108+
109+ func newSingBoxPlatform (tunFD int ) * singBoxPlatform {
110+ return & singBoxPlatform {
111+ tunFD : tunFD ,
112+ monitor : newSingBoxInterfaceMonitor (),
113+ }
73114}
74115
75116func (p * singBoxPlatform ) Initialize (_ adapter.NetworkManager ) error { return nil }
@@ -99,46 +140,126 @@ func (p *singBoxPlatform) OpenInterface(options *tun.Options, _ option.TunPlatfo
99140
100141func (p * singBoxPlatform ) UsePlatformDefaultInterfaceMonitor () bool { return true }
101142func (p * singBoxPlatform ) CreateDefaultInterfaceMonitor (_ logger.Logger ) tun.DefaultInterfaceMonitor {
102- return & singBoxInterfaceMonitor {}
143+ return p . monitor
103144}
104145
105- func (p * singBoxPlatform ) UsePlatformNetworkInterfaces () bool { return false }
146+ func (p * singBoxPlatform ) UsePlatformNetworkInterfaces () bool { return false }
106147func (p * singBoxPlatform ) NetworkInterfaces () ([]adapter.NetworkInterface , error ) { return nil , os .ErrInvalid }
107148
108149func (p * singBoxPlatform ) UnderNetworkExtension () bool { return true }
109150func (p * singBoxPlatform ) NetworkExtensionIncludeAllNetworks () bool { return false }
110151
111- func (p * singBoxPlatform ) ClearDNSCache () {}
112- func (p * singBoxPlatform ) RequestPermissionForWIFIState () error { return nil }
113- func (p * singBoxPlatform ) ReadWIFIState () adapter.WIFIState { return adapter.WIFIState {} }
114- func (p * singBoxPlatform ) SystemCertificates () []string { return nil }
152+ func (p * singBoxPlatform ) ClearDNSCache () {}
153+ func (p * singBoxPlatform ) RequestPermissionForWIFIState () error { return nil }
154+ func (p * singBoxPlatform ) ReadWIFIState () adapter.WIFIState { return adapter.WIFIState {} }
155+ func (p * singBoxPlatform ) SystemCertificates () []string { return nil }
115156
116157func (p * singBoxPlatform ) UsePlatformConnectionOwnerFinder () bool { return false }
117158func (p * singBoxPlatform ) FindConnectionOwner (_ * adapter.FindConnectionOwnerRequest ) (* adapter.ConnectionOwner , error ) {
118159 return nil , os .ErrInvalid
119160}
120161
121- func (p * singBoxPlatform ) UsePlatformWIFIMonitor () bool { return false }
122- func (p * singBoxPlatform ) UsePlatformNotification () bool { return false }
123- func (p * singBoxPlatform ) SendNotification (_ * adapter.Notification ) error { return nil }
124- func (p * singBoxPlatform ) MyInterfaceAddress () []netip.Addr { return p .myAddresses }
125-
126- // singBoxInterfaceMonitor is a no-op DefaultInterfaceMonitor. The
127- // network manager calls it when running with a platform interface,
128- // but in NEPacketTunnelProvider we let iOS handle outbound routing
129- // — sing-box does not need to bind sockets to a specific underlying
130- // interface. Outbound auto-detect-interface options become no-ops.
131- type singBoxInterfaceMonitor struct {}
132-
133- func (* singBoxInterfaceMonitor ) Start () error { return nil }
134- func (* singBoxInterfaceMonitor ) Close () error { return nil }
135- func (* singBoxInterfaceMonitor ) DefaultInterface () * control.Interface { return nil }
136- func (* singBoxInterfaceMonitor ) OverrideAndroidVPN () bool { return false }
137- func (* singBoxInterfaceMonitor ) AndroidVPNEnabled () bool { return false }
138- func (* singBoxInterfaceMonitor ) RegisterCallback (_ tun.DefaultInterfaceUpdateCallback ) * list.Element [tun.DefaultInterfaceUpdateCallback ] {
139- return nil
162+ func (p * singBoxPlatform ) UsePlatformWIFIMonitor () bool { return false }
163+ func (p * singBoxPlatform ) UsePlatformNotification () bool { return false }
164+ func (p * singBoxPlatform ) SendNotification (_ * adapter.Notification ) error { return nil }
165+ func (p * singBoxPlatform ) MyInterfaceAddress () []netip.Addr { return p .myAddresses }
166+
167+ // singBoxInterfaceMonitor is a tun.DefaultInterfaceMonitor driven from
168+ // outside Go — typically by an NWPathMonitor on the iOS side feeding
169+ // Evcore.UpdateDefaultInterface. Without this, sing-box's router never
170+ // learns that the underlying network changed and sockets pinned to a
171+ // stale path keep retransmitting until OS-level timeouts.
172+ //
173+ // Layout mirrors libbox's `platformDefaultInterfaceMonitor`
174+ // (experimental/libbox/monitor.go): refresh sing-box's cached
175+ // interface list, look the new default up by index, fan callbacks
176+ // out under a mutex. When the index is -1 we signal `no path` by
177+ // calling callbacks with a nil interface, which the network manager
178+ // translates into `pauseManager.NetworkPause`.
179+ type singBoxInterfaceMonitor struct {
180+ access sync.Mutex
181+ current * control.Interface
182+ myInterface string
183+ callbacks list.List [tun.DefaultInterfaceUpdateCallback ]
184+ }
185+
186+ func newSingBoxInterfaceMonitor () * singBoxInterfaceMonitor {
187+ return & singBoxInterfaceMonitor {}
188+ }
189+
190+ func (m * singBoxInterfaceMonitor ) Start () error { return nil }
191+ func (m * singBoxInterfaceMonitor ) Close () error { return nil }
192+ func (m * singBoxInterfaceMonitor ) OverrideAndroidVPN () bool { return false }
193+ func (m * singBoxInterfaceMonitor ) AndroidVPNEnabled () bool { return false }
194+
195+ func (m * singBoxInterfaceMonitor ) DefaultInterface () * control.Interface {
196+ m .access .Lock ()
197+ defer m .access .Unlock ()
198+ return m .current
199+ }
200+
201+ func (m * singBoxInterfaceMonitor ) RegisterCallback (callback tun.DefaultInterfaceUpdateCallback ) * list.Element [tun.DefaultInterfaceUpdateCallback ] {
202+ m .access .Lock ()
203+ defer m .access .Unlock ()
204+ return m .callbacks .PushBack (callback )
140205}
141- func (* singBoxInterfaceMonitor ) UnregisterCallback (_ * list.Element [tun.DefaultInterfaceUpdateCallback ]) {
206+
207+ func (m * singBoxInterfaceMonitor ) UnregisterCallback (element * list.Element [tun.DefaultInterfaceUpdateCallback ]) {
208+ m .access .Lock ()
209+ defer m .access .Unlock ()
210+ m .callbacks .Remove (element )
211+ }
212+
213+ func (m * singBoxInterfaceMonitor ) RegisterMyInterface (interfaceName string ) {
214+ m .access .Lock ()
215+ defer m .access .Unlock ()
216+ m .myInterface = interfaceName
217+ }
218+
219+ func (m * singBoxInterfaceMonitor ) MyInterface () string {
220+ m .access .Lock ()
221+ defer m .access .Unlock ()
222+ return m .myInterface
223+ }
224+
225+ // updateDefault sets the monitor's default interface and fans out a
226+ // callback. nm is used to refresh sing-box's cached interface list
227+ // and resolve the new default by index — when the manager is missing
228+ // (start-up race) or the lookup fails we still fire a synthetic
229+ // control.Interface so the router gets at least a name/index.
230+ func (m * singBoxInterfaceMonitor ) updateDefault (nm adapter.NetworkManager , name string , index int32 , _ bool , _ bool ) {
231+ if nm != nil {
232+ _ = nm .UpdateInterfaces ()
233+ }
234+
235+ var newInterface * control.Interface
236+ if index >= 0 {
237+ if nm != nil {
238+ if iif , err := nm .InterfaceFinder ().ByIndex (int (index )); err == nil {
239+ newInterface = iif
240+ }
241+ }
242+ if newInterface == nil {
243+ newInterface = & control.Interface {Name : name , Index : int (index )}
244+ }
245+ }
246+
247+ m .access .Lock ()
248+ old := m .current
249+ m .current = newInterface
250+ if newInterface != nil && old != nil && old .Index == newInterface .Index && old .Name == newInterface .Name {
251+ // Identical to the previous default — no need to fan callbacks
252+ // out and trigger a ResetNetwork.
253+ m .access .Unlock ()
254+ return
255+ }
256+ callbacks := make ([]tun.DefaultInterfaceUpdateCallback , 0 , m .callbacks .Len ())
257+ for el := m .callbacks .Front (); el != nil ; el = el .Next () {
258+ callbacks = append (callbacks , el .Value )
259+ }
260+ m .access .Unlock ()
261+
262+ for _ , callback := range callbacks {
263+ callback (newInterface , 0 )
264+ }
142265}
143- func (* singBoxInterfaceMonitor ) RegisterMyInterface (_ string ) {}
144- func (* singBoxInterfaceMonitor ) MyInterface () string { return "" }
0 commit comments