diff --git a/internal/sub/remote_routing.go b/internal/sub/remote_routing.go index 90113e4ed..b455e0be2 100644 --- a/internal/sub/remote_routing.go +++ b/internal/sub/remote_routing.go @@ -486,7 +486,9 @@ func newRemoteRoutingHTTPClient() *http.Client { ForceAttemptHTTP2: true, TLSHandshakeTimeout: 4 * time.Second, ResponseHeaderTimeout: 5 * time.Second, - TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + // Downloads are minutes apart, and a parked connection lives as long as the host likes. + DisableKeepAlives: true, + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, } return &http.Client{ Timeout: remoteRoutingHTTPTimeout, diff --git a/internal/web/service/node.go b/internal/web/service/node.go index 0b6442097..a3463966e 100644 --- a/internal/web/service/node.go +++ b/internal/web/service/node.go @@ -91,6 +91,7 @@ func (s *NodeService) FetchCertFingerprint(ctx context.Context, n *model.Node) ( TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // lgtm[go/disabled-certificate-check] }, } + defer client.CloseIdleConnections() resp, err := client.Do(req) if err != nil { return "", err diff --git a/internal/web/service/node_test.go b/internal/web/service/node_test.go index ddddc0b2a..bd18c9c82 100644 --- a/internal/web/service/node_test.go +++ b/internal/web/service/node_test.go @@ -1,7 +1,15 @@ package service import ( + "context" + "net" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "sync/atomic" "testing" + "time" "github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" @@ -250,3 +258,33 @@ func TestFilterNodeSnapshotKeepsAdoptedAlias(t *testing.T) { t.Fatalf("filtered snapshot = %#v, want adopted alias only", snap.Inbounds) } } + +// The probe builds a transport per call; what it parks would stay open for as long +// as the node, an untrusted peer, keeps the connection. +func TestFetchCertFingerprintLeavesNoIdleConnection(t *testing.T) { + var open atomic.Int32 + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + server.Config.ConnState = func(_ net.Conn, state http.ConnState) { + switch state { + case http.StateNew: + open.Add(1) + case http.StateClosed, http.StateHijacked: + open.Add(-1) + } + } + server.StartTLS() + t.Cleanup(server.Close) + u, _ := url.Parse(server.URL) + port, _ := strconv.Atoi(u.Port()) + node := &model.Node{Scheme: "https", Address: u.Hostname(), Port: port, AllowPrivateAddress: true} + if _, err := (&NodeService{}).FetchCertFingerprint(context.Background(), node); err != nil { + t.Fatalf("FetchCertFingerprint: %v", err) + } + deadline := time.Now().Add(2 * time.Second) + for open.Load() != 0 && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } + if n := open.Load(); n != 0 { + t.Fatalf("%d connection(s) to the node still open after the probe", n) + } +}