feat(outbound): add real-delay connection test mode

The HTTP probe reports the warm per-request round-trip, which reads
lower than the delay figure client apps show for the same server. Add a
third "real" test mode that reuses the temp-instance HTTP probe but
reports the cold request's full elapsed time - tunnel establishment
included - and skips the warm request. UDP-transport outbounds forced
out of the TCP lane still report "http"; in real mode they report
"real". The mode joins the TCP/HTTP toggle on the outbounds tab, with
the label translated in all 13 locales.
This commit is contained in:
MHSanaei
2026-07-06 08:35:48 +02:00
parent 5a7b3b7370
commit ed66209e38
24 changed files with 188 additions and 58 deletions
@@ -464,6 +464,94 @@ func TestTestOutboundsHTTPBatchThroughStubSocks(t *testing.T) {
}
}
func TestTestOutboundsRealDelayBatchThroughStubSocks(t *testing.T) {
var mu sync.Mutex
requestsPerConn := make(map[string]int)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
requestsPerConn[r.RemoteAddr]++
mu.Unlock()
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
return &stubProcess{cfg: cfg, serveSocks: true}
})
batch := mustJSON(t, []any{
map[string]any{"tag": "a", "protocol": "vless"},
map[string]any{"tag": "wg", "protocol": "wireguard"},
})
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "real")
if err != nil {
t.Fatalf("TestOutbounds: %v", err)
}
for i, r := range results {
if !r.Success {
t.Fatalf("result %d failed: %+v", i, r)
}
if r.Mode != "real" {
t.Errorf("result %d mode = %q, want %q", i, r.Mode, "real")
}
if r.HTTPStatus != http.StatusNoContent {
t.Errorf("result %d status = %d, want 204", i, r.HTTPStatus)
}
if r.Delay < 1 || r.ConnectMs < 1 || r.TTFBMs < 1 {
t.Errorf("result %d timing not populated: %+v", i, r)
}
}
mu.Lock()
defer mu.Unlock()
totalRequests := 0
for addr, n := range requestsPerConn {
totalRequests += n
if n != 1 {
t.Errorf("connection %s served %d requests, want 1 (real mode must skip the warm request)", addr, n)
}
}
if totalRequests != 2 {
t.Errorf("test URL served %d requests, want 2 (one cold request per probe)", totalRequests)
}
}
func TestTestOutboundsTCPModeForcesUDPToHTTPProbe(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}))
defer srv.Close()
withStubProcess(t, func(cfg *xray.Config, configPath string) batchProcess {
return &stubProcess{cfg: cfg, serveSocks: true}
})
batch := mustJSON(t, []any{map[string]any{"tag": "wg", "protocol": "wireguard"}})
results, err := (&OutboundService{}).TestOutbounds(batch, srv.URL, "", "tcp")
if err != nil {
t.Fatalf("TestOutbounds: %v", err)
}
r := results[0]
if !r.Success || r.Mode != "http" {
t.Errorf("UDP outbound in tcp mode = %+v, want success with mode %q", r, "http")
}
}
func TestProbeModeLabel(t *testing.T) {
cases := []struct{ mode, want string }{
{"tcp", "tcp"},
{"real", "real"},
{"http", "http"},
{"", "http"},
{"bogus", "http"},
}
for _, c := range cases {
if got := probeModeLabel(c.mode); got != c.want {
t.Errorf("probeModeLabel(%q) = %q, want %q", c.mode, got, c.want)
}
}
}
func TestProbeThroughSocksTransportFailure(t *testing.T) {
// A listener that accepts and immediately closes — SOCKS handshake dies.
l, err := net.Listen("tcp", "127.0.0.1:0")
@@ -482,7 +570,7 @@ func TestProbeThroughSocksTransportFailure(t *testing.T) {
}()
var result TestOutboundResult
probeThroughSocks(l.Addr().(*net.TCPAddr).Port, "http://127.0.0.1:9/", 2*time.Second, &result)
probeThroughSocks(l.Addr().(*net.TCPAddr).Port, "http://127.0.0.1:9/", 2*time.Second, false, &result)
if result.Success || result.Error == "" {
t.Errorf("expected transport failure, got %+v", result)
}