Skip to content

Commit 19eb4d5

Browse files
committed
Add inactivity timeout support
1 parent 0525264 commit 19eb4d5

File tree

4 files changed

+399
-0
lines changed

4 files changed

+399
-0
lines changed

box.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ func New(options Options) (*Box, error) {
379379
timeService.TimeService = ntpService
380380
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
381381
}
382+
if routeOptions.IdleTimeout > 0 {
383+
activityTracker := route.NewActivityTracker(
384+
logFactory.NewLogger("activity"),
385+
time.Duration(routeOptions.IdleTimeout),
386+
)
387+
router.AppendTracker(activityTracker)
388+
internalServices = append(internalServices, adapter.NewLifecycleService(activityTracker, "activity tracker"))
389+
}
382390
return &Box{
383391
network: networkManager,
384392
endpoint: endpointManager,

option/route.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type RouteOptions struct {
1818
DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"`
1919
DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"`
2020
DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"`
21+
IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"`
2122
}
2223

2324
type GeoIPOptions struct {

route/activity_tracker.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package route
2+
3+
import (
4+
"context"
5+
"net"
6+
"os"
7+
"sync/atomic"
8+
"time"
9+
10+
"github.com/sagernet/sing-box/adapter"
11+
"github.com/sagernet/sing/common/bufio"
12+
"github.com/sagernet/sing/common/logger"
13+
N "github.com/sagernet/sing/common/network"
14+
)
15+
16+
var _ adapter.ConnectionTracker = (*ActivityTracker)(nil)
17+
18+
type ActivityTracker struct {
19+
logger logger.ContextLogger
20+
timeout time.Duration
21+
checkInterval time.Duration // for testing
22+
lastActivity atomic.Int64 // Unix nano timestamp
23+
done chan struct{}
24+
exitFunc func() // for testing
25+
}
26+
27+
func NewActivityTracker(logger logger.ContextLogger, timeout time.Duration) *ActivityTracker {
28+
tracker := &ActivityTracker{
29+
logger: logger,
30+
timeout: timeout,
31+
checkInterval: 10 * time.Second,
32+
done: make(chan struct{}),
33+
exitFunc: func() {
34+
os.Exit(0)
35+
},
36+
}
37+
tracker.lastActivity.Store(time.Now().UnixNano())
38+
return tracker
39+
}
40+
41+
func (t *ActivityTracker) updateActivity(n int64) {
42+
if n > 0 {
43+
t.lastActivity.Store(time.Now().UnixNano())
44+
}
45+
}
46+
47+
func (t *ActivityTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
48+
activityCounter := func(n int64) {
49+
t.updateActivity(n)
50+
}
51+
return bufio.NewCounterConn(conn,
52+
[]N.CountFunc{activityCounter},
53+
[]N.CountFunc{activityCounter})
54+
}
55+
56+
func (t *ActivityTracker) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn {
57+
activityCounter := func(n int64) {
58+
t.updateActivity(n)
59+
}
60+
return bufio.NewCounterPacketConn(conn,
61+
[]N.CountFunc{activityCounter},
62+
[]N.CountFunc{activityCounter})
63+
}
64+
65+
func (t *ActivityTracker) Start() error {
66+
go t.monitorActivity()
67+
return nil
68+
}
69+
70+
func (t *ActivityTracker) Close() error {
71+
select {
72+
case <-t.done:
73+
return nil
74+
default:
75+
close(t.done)
76+
}
77+
return nil
78+
}
79+
80+
func (t *ActivityTracker) monitorActivity() {
81+
ticker := time.NewTicker(t.checkInterval)
82+
defer ticker.Stop()
83+
84+
for {
85+
select {
86+
case <-t.done:
87+
return
88+
case <-ticker.C:
89+
lastActive := time.Unix(0, t.lastActivity.Load())
90+
idleDuration := time.Since(lastActive)
91+
92+
if idleDuration >= t.timeout {
93+
t.logger.Info("idle timeout reached after ", idleDuration.String(), " of inactivity, exiting")
94+
t.exitFunc()
95+
}
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)