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.
This commit is contained in:
MHSanaei 2026-06-24 21:35:34 +02:00
parent 98efa43220
commit 768ba1a6f5
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
2 changed files with 11 additions and 73 deletions

View file

@ -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":

View file

@ -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) {