From 825778144c449ad5ab0c7a5714402ce78ad25b3b Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Thu, 11 Jun 2026 22:49:22 +0200 Subject: [PATCH] fix(outbound): widen probe timeout and surface failure reason in outbound test (#5152) The v3 outbound test spins up a temp xray that probes the outbound via burstObservatory. Two regressions made it report "Failed" for healthy outbounds on high-latency / tunnel-routed boxes (e.g. default route over an OpenVPN tun device to a remote proxy), even though client traffic over the same outbound works: - Each probe disables keep-alive, so every attempt is a cold round-trip (redial + re-handshake). The 5s per-probe timeout was too tight for such paths and every probe timed out. Restore the ~10s budget the pre-v3 SOCKS-based test gave a cold connection (timeout 5s -> 10s) and widen the poll window 12s -> 15s so one full probe can complete and surface alive. - The temp config set log error to "none", discarding the real failure reason, so "Failed" was undiagnosable. Route error logs to stderr ("") like the production template does, so the probe error (DNS lookup failure, connection refused, deadline exceeded, TLS error, ...) is captured into the panel/Xray log, and point the operator there in the generic timeout messages. --- internal/web/service/outbound/outbound.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/internal/web/service/outbound/outbound.go b/internal/web/service/outbound/outbound.go index 1cef3d85e..88f0183ce 100644 --- a/internal/web/service/outbound/outbound.go +++ b/internal/web/service/outbound/outbound.go @@ -399,7 +399,7 @@ func (s *OutboundService) testOutboundHTTP(outboundJSON string, testURL string, return &TestOutboundResult{Mode: "http", Success: false, Error: fmt.Sprintf("Xray process exited: %s", result)}, nil } - return pollObservatoryResult(testProcess, metricsPort, outboundTag, 12*time.Second), nil + return pollObservatoryResult(testProcess, metricsPort, outboundTag, 15*time.Second), nil } // outboundsContainTag reports whether any outbound in the slice has the given tag. @@ -414,11 +414,6 @@ func outboundsContainTag(outbounds []any, tag string) bool { return false } -// createTestConfig builds a probe-only xray config: the original outbounds -// are kept as-is so dialerProxy chains still resolve, a burstObservatory -// is wired to probe the target tag, and a metrics listener exposes the -// observatory snapshot via /debug/vars. No inbound or routing rules are -// needed — burstObservatory issues the probe traffic itself. func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []any, metricsPort int, probeURL string) *xray.Config { processedOutbounds := make([]any, len(allOutbounds)) for i, ob := range allOutbounds { @@ -449,7 +444,7 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []an "destination": probeURL, "interval": "1s", "connectivity": "", - "timeout": "5s", + "timeout": "10s", "samplingCount": 1, }, }) @@ -462,7 +457,7 @@ func (s *OutboundService) createTestConfig(outboundTag string, allOutbounds []an logConfig := map[string]any{ "loglevel": "warning", "access": "none", - "error": "none", + "error": "", "dnsLog": false, } logJSON, _ := json.Marshal(logConfig) @@ -491,11 +486,6 @@ type observatoryEntry struct { OutboundTag string `json:"outbound_tag"` } -// pollObservatoryResult repeatedly reads /debug/vars and returns as soon -// as the target outbound reports alive=true. burstObservatory updates the -// snapshot after each ping (interval=1s, timeout=5s), so a healthy -// outbound usually surfaces within ~2s and the timeout caps the wait for -// truly dead ones. func pollObservatoryResult(testProcess *xray.Process, metricsPort int, tag string, timeout time.Duration) *TestOutboundResult { url := fmt.Sprintf("http://127.0.0.1:%d/debug/vars", metricsPort) client := &http.Client{Timeout: 2 * time.Second} @@ -522,9 +512,9 @@ func pollObservatoryResult(testProcess *xray.Process, metricsPort int, tag strin time.Sleep(400 * time.Millisecond) } - msg := "Probe timed out — outbound did not become reachable" + msg := "Probe timed out — outbound did not become reachable (see Xray log for details)" if sawEntry && lastEntry.LastTryTime > 0 { - msg = fmt.Sprintf("All probes failed (last attempt %ds ago)", time.Now().Unix()-lastEntry.LastTryTime) + msg = fmt.Sprintf("All probes failed (last attempt %ds ago; see Xray log for details)", time.Now().Unix()-lastEntry.LastTryTime) } return &TestOutboundResult{Mode: "http", Success: false, Error: msg} }