Fix URLTest group hang on unresponsive outbound

This commit is contained in:
世界
2026-08-21 18:21:51 +08:00
parent 9d8d4b8796
commit e4a4e8c790
6 changed files with 84 additions and 15 deletions
+1
View File
@@ -143,6 +143,7 @@ type OutboundGroup interface {
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context) (map[string]uint16, error)
PerformUpdateCheck()
}
func OutboundTag(detour Outbound) string {
+34
View File
@@ -184,6 +184,30 @@ func updateProxy(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}
func groupContains(outboundManager adapter.OutboundManager, outboundGroup adapter.OutboundGroup, tag string, visited map[string]bool) bool {
for _, memberTag := range outboundGroup.All() {
if memberTag == tag {
return true
}
member, loaded := outboundManager.Outbound(memberTag)
if !loaded {
continue
}
if group.RealTag(member) == tag {
return true
}
memberGroup, isGroup := member.(adapter.OutboundGroup)
if !isGroup || visited[memberTag] {
continue
}
visited[memberTag] = true
if groupContains(outboundManager, memberGroup, tag, visited) {
return true
}
}
return false
}
func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
@@ -213,6 +237,16 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
Delay: delay,
})
}
for _, detour := range server.outbound.Outbounds() {
urlTestGroup, isURLTestGroup := detour.(adapter.URLTestGroup)
if !isURLTestGroup {
continue
}
if !groupContains(server.outbound, urlTestGroup, realTag, map[string]bool{detour.Tag(): true}) {
continue
}
urlTestGroup.PerformUpdateCheck()
}
}()
if ctx.Err() != nil {
+1 -1
View File
@@ -34,7 +34,7 @@ require (
github.com/sagernet/gomobile v0.1.12
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.13
github.com/sagernet/sing v0.8.14
github.com/sagernet/sing-mux v0.3.5
github.com/sagernet/sing-quic v0.6.4-0.20260803041914-d83826c306d7
github.com/sagernet/sing-shadowsocks v0.2.8
+2 -2
View File
@@ -236,8 +236,8 @@ github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.13 h1:yVoXnx9nPxfjlwD4Tp+Wd9zuW2tfiSVrcRDBZNbKRCw=
github.com/sagernet/sing v0.8.13/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing v0.8.14 h1:S6Netv4F61uNAuD/sUbHnGuNUEPwtL08Ouk0//CVYgM=
github.com/sagernet/sing v0.8.14/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.6.4-0.20260803041914-d83826c306d7 h1:D46kmyvKMNVFvL3KXdq3T4vy8r29xCMmSqNuQHJBybE=
+28 -6
View File
@@ -115,6 +115,10 @@ func (s *URLTest) CheckOutbounds() {
s.group.CheckOutbounds(true)
}
func (s *URLTest) PerformUpdateCheck() {
s.group.performUpdateCheck()
}
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
s.group.Touch()
var outbound adapter.Outbound
@@ -202,6 +206,7 @@ type URLTestGroup struct {
interruptGroup *interrupt.Group
interruptExternalConnections bool
access sync.Mutex
updateAccess sync.Mutex
ticker *time.Ticker
close chan struct{}
started bool
@@ -362,6 +367,11 @@ func (g *URLTestGroup) URLTest(ctx context.Context) (map[string]uint16, error) {
return g.urlTest(ctx, false)
}
type urlTestResult struct {
delay uint16
err error
}
func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint16, error) {
result := make(map[string]uint16)
if g.checking.Swap(true) {
@@ -389,18 +399,28 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
b.Go(realTag, func() (any, error) {
testCtx, cancel := context.WithTimeout(g.ctx, C.TCPTimeout)
defer cancel()
t, err := urltest.URLTest(testCtx, g.link, p)
if err != nil {
g.logger.Debug("outbound ", tag, " unavailable: ", err)
testChan := make(chan urlTestResult, 1)
go func() {
delay, testErr := urltest.URLTest(testCtx, g.link, p)
testChan <- urlTestResult{delay, testErr}
}()
var testResult urlTestResult
select {
case testResult = <-testChan:
case <-testCtx.Done():
testResult.err = testCtx.Err()
}
if testResult.err != nil {
g.logger.Debug("outbound ", tag, " unavailable: ", testResult.err)
g.history.DeleteURLTestHistory(realTag)
} else {
g.logger.Debug("outbound ", tag, " available: ", t, "ms")
g.logger.Debug("outbound ", tag, " available: ", testResult.delay, "ms")
g.history.StoreURLTestHistory(realTag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
Delay: testResult.delay,
})
resultAccess.Lock()
result[tag] = t
result[tag] = testResult.delay
resultAccess.Unlock()
}
return nil, nil
@@ -412,6 +432,8 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint
}
func (g *URLTestGroup) performUpdateCheck() {
g.updateAccess.Lock()
defer g.updateAccess.Unlock()
var updated bool
if outbound, exists := g.Select(N.NetworkTCP); outbound != nil && (g.selectedOutboundTCP == nil || (exists && outbound != g.selectedOutboundTCP)) {
if g.selectedOutboundTCP != nil {
+18 -6
View File
@@ -35,7 +35,6 @@ var _ adapter.InterfaceUpdateListener = (*Outbound)(nil)
type Outbound struct {
outbound.Adapter
ctx context.Context
logger logger.ContextLogger
dialer N.Dialer
serverAddr M.Socksaddr
@@ -56,7 +55,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
}
outbound := &Outbound{
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, tag, []string{N.NetworkTCP}, options.DialerOptions),
ctx: ctx,
logger: logger,
dialer: outboundDialer,
serverAddr: options.ServerOptions.Build(),
@@ -121,7 +119,7 @@ func randomVersion() string {
return version
}
func (s *Outbound) connect() (*ssh.Client, error) {
func (s *Outbound) connect(ctx context.Context) (client *ssh.Client, err error) {
if s.client != nil {
return s.client, nil
}
@@ -133,10 +131,24 @@ func (s *Outbound) connect() (*ssh.Client, error) {
return s.client, nil
}
conn, err := s.dialer.DialContext(s.ctx, N.NetworkTCP, s.serverAddr)
conn, err := s.dialer.DialContext(ctx, N.NetworkTCP, s.serverAddr)
if err != nil {
return nil, err
}
if ctx.Done() != nil {
handshakeConn := conn
stopContext := context.AfterFunc(ctx, func() {
_ = handshakeConn.Close()
})
defer func() {
if !stopContext() {
s.client = nil
s.clientConn = nil
client = nil
err = ctx.Err()
}
}()
}
config := &ssh.ClientConfig{
User: s.user,
Auth: s.authMethod,
@@ -161,7 +173,7 @@ func (s *Outbound) connect() (*ssh.Client, error) {
return nil, E.Cause(err, "connect to ssh server")
}
client := ssh.NewClient(clientConn, chans, reqs)
client = ssh.NewClient(clientConn, chans, reqs)
s.clientConn = conn
s.client = client
@@ -187,7 +199,7 @@ func (s *Outbound) Close() error {
}
func (s *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
client, err := s.connect()
client, err := s.connect(ctx)
if err != nil {
return nil, err
}