mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-15 10:52:40 +03:00
fix(outbound): read the probe protocol id and transport name like the core (#6526)
* fix(outbound): read the probe protocol id and transport name like the core The probe lane gate and the endpoint extractor behind it compared both strings exactly, so a template the core is running was probed as something else. With mode=tcp an outbound spelled "WireGuard" stayed in the dial-only TCP lane, where extractOutboundEndpoints matched no case and the caller got "No testable endpoint" for an outbound that is passing traffic. The core lowercases a protocol id (infra/conf/loader.go) and a transport name (TransportProtocol.Build) before it resolves either, and resolves both "kcp" and "mkcp" to mKCP, so both readers now normalise the same way. The panel no longer reaches the lane gate itself — the browser now sends http for these outbounds — but the endpoint documents "tcp" for fast dial-only probes with UDP-transport outbounds still probed over HTTP, and that promise has to hold for direct API callers too. * fix(outbound): read the batch probe protocol id like the core Review of #6526 found that folding "WireGuard"/"AmneziaWG" into the UDP lane newly routed those spellings onto two readers in buildBatchTestConfig that still compared the id exactly. A case-variant WireGuard outbound therefore reached the temp probe instance without noKernelTun -- which on Linux creates a kernel TUN device alongside the live panel's own -- and a case-variant AmneziaWG entry was appended raw, rejecting the whole temp config and degrading the batch to serial per-item retries. Both readers now fold the id the way the core does (infra/conf/loader.go lowercases it before the protocol is resolved).
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -221,11 +222,23 @@ func probeTCPEndpoint(endpoint string, timeout time.Duration) TestEndpointResult
|
||||
// dial neither proves reachability nor measures latency. Such outbounds
|
||||
// must go through the real xray handshake probe instead.
|
||||
func outboundTransportIsUDP(ob map[string]any) bool {
|
||||
if protocol, _ := ob["protocol"].(string); protocol == "hysteria" || protocol == "wireguard" || protocol == "amneziawg" {
|
||||
if protocol, _ := ob["protocol"].(string); equalsAnyFold(protocol, "hysteria", "wireguard", "amneziawg") {
|
||||
return true
|
||||
}
|
||||
if stream, ok := ob["streamSettings"].(map[string]any); ok {
|
||||
if n, _ := stream["network"].(string); n == "hysteria" || n == "kcp" || n == "quic" {
|
||||
// The core resolves "kcp" and "mkcp" to the same mKCP transport.
|
||||
if n, _ := stream["network"].(string); equalsAnyFold(n, "hysteria", "kcp", "mkcp", "quic") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// equalsAnyFold mirrors the core, which lowercases a protocol id and a
|
||||
// transport name before it resolves either of them.
|
||||
func equalsAnyFold(value string, want ...string) bool {
|
||||
for _, w := range want {
|
||||
if strings.EqualFold(value, w) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -234,6 +247,7 @@ func outboundTransportIsUDP(ob map[string]any) bool {
|
||||
|
||||
func extractOutboundEndpoints(ob map[string]any) []string {
|
||||
protocol, _ := ob["protocol"].(string)
|
||||
protocol = strings.ToLower(protocol)
|
||||
settings, _ := ob["settings"].(map[string]any)
|
||||
if settings == nil {
|
||||
return nil
|
||||
|
||||
@@ -394,7 +394,7 @@ func buildBatchTestConfig(items []*httpBatchItem, allOutbounds []any, ports []in
|
||||
bridged = append(bridged, ob)
|
||||
continue
|
||||
}
|
||||
if p, _ := m["protocol"].(string); p != "amneziawg" {
|
||||
if p, _ := m["protocol"].(string); !strings.EqualFold(p, "amneziawg") {
|
||||
bridged = append(bridged, ob)
|
||||
continue
|
||||
}
|
||||
@@ -418,7 +418,7 @@ func buildBatchTestConfig(items []*httpBatchItem, allOutbounds []any, ports []in
|
||||
continue
|
||||
}
|
||||
// The temp instance must not touch kernel WireGuard devices.
|
||||
if protocol, ok := outbound["protocol"].(string); ok && protocol == "wireguard" {
|
||||
if protocol, ok := outbound["protocol"].(string); ok && strings.EqualFold(protocol, "wireguard") {
|
||||
if settings, ok := outbound["settings"].(map[string]any); ok {
|
||||
settings["noKernelTun"] = true
|
||||
} else {
|
||||
|
||||
143
internal/web/service/outbound/probe_protocol_case_test.go
Normal file
143
internal/web/service/outbound/probe_protocol_case_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
)
|
||||
|
||||
// The core lowercases a protocol id and a transport name before it resolves
|
||||
// either, so every reader here has to accept the spelling the core accepts.
|
||||
|
||||
func TestTestOutboundsTCPModeForcesCoreSpelledUDPToHTTPProbe(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}
|
||||
})
|
||||
withEgressTraceProbe(t, func(*url.URL) *TestEgressResult {
|
||||
return &TestEgressResult{IPv4: "198.51.100.2", Country: "ZZ", Warp: "off"}
|
||||
})
|
||||
|
||||
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(`"WireGuard" outbound in tcp mode = %+v, want success with mode %q`, r, "http")
|
||||
}
|
||||
if r.Egress == nil || r.Egress.IPv4 != "198.51.100.2" {
|
||||
t.Errorf(`"WireGuard" outbound egress = %+v`, r.Egress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutboundTransportIsUDPMatchesTheCore(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ob map[string]any
|
||||
want bool
|
||||
}{
|
||||
{"canonical wireguard", map[string]any{"protocol": "wireguard"}, true},
|
||||
{"capitalised wireguard", map[string]any{"protocol": "WireGuard"}, true},
|
||||
{"upper hysteria", map[string]any{"protocol": "HYSTERIA"}, true},
|
||||
{"amneziawg", map[string]any{"protocol": "amneziawg"}, true},
|
||||
{"kcp transport", map[string]any{"streamSettings": map[string]any{"network": "kcp"}}, true},
|
||||
{"kcp transport capitalised", map[string]any{"streamSettings": map[string]any{"network": "KCP"}}, true},
|
||||
{"mkcp alias", map[string]any{"streamSettings": map[string]any{"network": "mkcp"}}, true},
|
||||
{"mkcp alias capitalised", map[string]any{"streamSettings": map[string]any{"network": "MKCP"}}, true},
|
||||
{"tcp transport", map[string]any{"streamSettings": map[string]any{"network": "tcp"}}, false},
|
||||
{"plain vless", map[string]any{"protocol": "vless"}, false},
|
||||
{"matched but tcp", map[string]any{"protocol": "vless", "streamSettings": map[string]any{"network": "ws"}}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := outboundTransportIsUDP(tt.ob); got != tt.want {
|
||||
t.Errorf("outboundTransportIsUDP(%v) = %v, want %v", tt.ob, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBatchTestConfigReadsTheProtocolIDLikeTheCore(t *testing.T) {
|
||||
items := []*httpBatchItem{
|
||||
{tag: "wg", outbound: map[string]any{"tag": "wg", "protocol": "WireGuard"}},
|
||||
{tag: "awg", outbound: map[string]any{"tag": "awg", "protocol": "AmneziaWG"}},
|
||||
}
|
||||
|
||||
cfg := buildBatchTestConfig(items, nil, []int{61011, 61012})
|
||||
raw, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal config: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatalf("unmarshal config: %v", err)
|
||||
}
|
||||
|
||||
outbounds, _ := m["outbounds"].([]any)
|
||||
byTag := make(map[string]map[string]any, len(outbounds))
|
||||
for _, entry := range outbounds {
|
||||
ob, _ := entry.(map[string]any)
|
||||
tag, _ := ob["tag"].(string)
|
||||
byTag[tag] = ob
|
||||
}
|
||||
|
||||
wg := byTag["wg"]
|
||||
if wg == nil {
|
||||
t.Fatalf("wg outbound missing from the temp config: %v", outbounds)
|
||||
}
|
||||
if settings, _ := wg["settings"].(map[string]any); settings == nil || settings["noKernelTun"] != true {
|
||||
t.Errorf(`"WireGuard" settings = %v, want noKernelTun: the probe instance must not create a kernel device`, wg["settings"])
|
||||
}
|
||||
|
||||
awg := byTag["awg"]
|
||||
if awg == nil {
|
||||
t.Fatalf("awg outbound missing from the temp config: %v", outbounds)
|
||||
}
|
||||
if protocol, _ := awg["protocol"].(string); protocol != "socks" {
|
||||
t.Errorf(`"AmneziaWG" protocol = %q, want %q: a raw amneziawg entry fails the whole temp config`, protocol, "socks")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestOutboundsTCPLaneReadsProtocolIDCaseInsensitively(t *testing.T) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer l.Close()
|
||||
go func() {
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
port := l.Addr().(*net.TCPAddr).Port
|
||||
|
||||
batch := mustJSON(t, []any{map[string]any{
|
||||
"tag": "t1",
|
||||
"protocol": "SOCKS",
|
||||
"settings": map[string]any{"servers": []any{map[string]any{"address": "127.0.0.1", "port": port}}},
|
||||
}})
|
||||
results, err := (&OutboundService{}).TestOutbounds(batch, "", "", "tcp")
|
||||
if err != nil {
|
||||
t.Fatalf("TestOutbounds: %v", err)
|
||||
}
|
||||
r := results[0]
|
||||
if !r.Success || r.Mode != "tcp" || len(r.Endpoints) != 1 {
|
||||
t.Errorf(`"SOCKS" outbound in tcp mode = %+v, want a successful tcp probe with one endpoint`, r)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user