mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-24 00:40:26 +00:00
Fix URLTest group hang on unresponsive outbound
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user