From 768ba1a6f577dcaeb91c69b2081aba8f1414abbb Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 24 Jun 2026 21:35:34 +0200 Subject: [PATCH] fix(tunnelmonitor): reuse netproxy client and init logger in tests Replace the duplicated newHTTPClient/dialContextWithProxy with netproxy.NewHTTPClient, which centralises the http/https/socks5 handling and avoids the dial-goroutine connection leak on context cancellation. Cap failures at the threshold during cooldown so the counter stays a true consecutive-failure count. Add TestMain to initialise the logger and fix the nil-pointer panic in the success-after-failure path. --- internal/tunnelmonitor/monitor.go | 76 +------------------------- internal/tunnelmonitor/monitor_test.go | 8 +++ 2 files changed, 11 insertions(+), 73 deletions(-) diff --git a/internal/tunnelmonitor/monitor.go b/internal/tunnelmonitor/monitor.go index 4f8800f76..ebb6ec6fe 100644 --- a/internal/tunnelmonitor/monitor.go +++ b/internal/tunnelmonitor/monitor.go @@ -5,16 +5,14 @@ import ( "errors" "fmt" "io" - "net" "net/http" - "net/url" "os" "strconv" "strings" "time" "github.com/mhsanaei/3x-ui/v3/internal/logger" - "golang.org/x/net/proxy" + "github.com/mhsanaei/3x-ui/v3/internal/util/netproxy" ) const ( @@ -116,7 +114,7 @@ func (c Config) Normalize() Config { func New(cfg Config, recover RecoveryFunc) (*Monitor, error) { cfg = cfg.Normalize() - client, err := newHTTPClient(cfg) + client, err := netproxy.NewHTTPClient(cfg.ProxyURL, cfg.Timeout) if err != nil { return nil, err } @@ -181,6 +179,7 @@ func (m *Monitor) Step(ctx context.Context) (bool, error) { now := m.now() if !m.lastRecovery.IsZero() && now.Sub(m.lastRecovery) < m.cfg.Cooldown { + m.failures = m.cfg.FailureThreshold return false, fmt.Errorf("probe failed %d/%d; recovery cooldown active: %w", m.failures, m.cfg.FailureThreshold, err) } @@ -225,75 +224,6 @@ func (m *Monitor) probe(ctx context.Context) error { return nil } -func newHTTPClient(cfg Config) (*http.Client, error) { - dialer := &net.Dialer{ - Timeout: cfg.Timeout, - KeepAlive: 30 * time.Second, - } - - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: dialer.DialContext, - ForceAttemptHTTP2: true, - TLSHandshakeTimeout: cfg.Timeout, - ResponseHeaderTimeout: cfg.Timeout, - ExpectContinueTimeout: time.Second, - } - - if cfg.ProxyURL != "" { - proxyURL, err := url.Parse(cfg.ProxyURL) - if err != nil { - return nil, fmt.Errorf("parse tunnel health proxy URL: %w", err) - } - - switch strings.ToLower(proxyURL.Scheme) { - case "http", "https": - transport.Proxy = http.ProxyURL(proxyURL) - case "socks5", "socks5h": - socksURL := *proxyURL - socksURL.Scheme = "socks5" - - socksDialer, err := proxy.FromURL(&socksURL, proxy.Direct) - if err != nil { - return nil, fmt.Errorf("create SOCKS proxy dialer: %w", err) - } - - transport.Proxy = nil - transport.DialContext = dialContextWithProxy(socksDialer) - default: - return nil, fmt.Errorf("unsupported tunnel health proxy scheme %q", proxyURL.Scheme) - } - } - - return &http.Client{ - Timeout: cfg.Timeout, - Transport: transport, - }, nil -} - -func dialContextWithProxy(dialer proxy.Dialer) func(context.Context, string, string) (net.Conn, error) { - return func(ctx context.Context, network string, address string) (net.Conn, error) { - type result struct { - conn net.Conn - err error - } - - ch := make(chan result, 1) - - go func() { - conn, err := dialer.Dial(network, address) - ch <- result{conn: conn, err: err} - }() - - select { - case <-ctx.Done(): - return nil, ctx.Err() - case res := <-ch: - return res.conn, res.err - } - } -} - func parseBool(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "y", "on", "enable", "enabled": diff --git a/internal/tunnelmonitor/monitor_test.go b/internal/tunnelmonitor/monitor_test.go index 587461ef4..b2657c308 100644 --- a/internal/tunnelmonitor/monitor_test.go +++ b/internal/tunnelmonitor/monitor_test.go @@ -7,8 +7,16 @@ import ( "strings" "testing" "time" + + "github.com/mhsanaei/3x-ui/v3/internal/logger" + "github.com/op/go-logging" ) +func TestMain(m *testing.M) { + logger.InitLogger(logging.ERROR) + m.Run() +} + type roundTripFunc func(*http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {