fix(xray): check outbounds the way the core's handlers will

ValidateOutboundConfig ran only conf.Build(), but the core parses some
fields again in its handler constructors, and a failure there stops the
whole core at start: wireguard local addresses and peers, the vless
encryption keys and padding, and shadowsocks 2022 keys. Mirror those
parses with the core's own helpers, and turn a panic in the loader into
a rejection so one bad link cannot unwind the caller.
This commit is contained in:
MHSanaei
2026-10-31 08:23:24 +00:00
parent 1fa8cc785d
commit 2d72d9c715
2 changed files with 154 additions and 7 deletions
+64
View File
@@ -1,10 +1,13 @@
package xray
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/util/link"
)
// TestValidateOutboundConfig_RejectsUnencryptedPublicVless covers xray-core
@@ -86,3 +89,64 @@ func TestValidateOutboundConfig_FindsANoiseScriptBesideTheXrayBinary(t *testing.
t.Fatalf("ValidateOutboundConfig = %v, want the script beside the xray binary found", err)
}
}
// The core's loader panics on some third-party values instead of returning an error;
// a subscription refresh must count that link as rejected, not unwind the whole job.
func TestValidateOutboundConfig_ReturnsTheLoaderPanicAsAnError(t *testing.T) {
for name, outbound := range map[string]string{
"vless encryption": `{"protocol": "vless",
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "mlkem768x25519plus.native.0rtt.AAAA"},
"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "example.com"}}}`,
"reality spiderX": `{"protocol": "vless",
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
"streamSettings": {"network": "tcp", "security": "reality", "realitySettings": {
"serverName": "example.com", "fingerprint": "chrome", "spiderX": "/%zz", "publicKey": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg"}}}`,
} {
if err := ValidateOutboundConfig([]byte(outbound)); err == nil || !strings.Contains(err.Error(), "panicked") {
t.Fatalf("%s: ValidateOutboundConfig = %v, want the loader's panic as an error", name, err)
}
}
}
// conf.Build() lets these through, but the handler constructor parses them at core
// start, and one failure there takes every inbound down with it.
func TestValidateOutboundConfig_RejectsLinksTheHandlerConstructorRefuses(t *testing.T) {
const wg = "wireguard://JOSLNr3Kk8BViwrrTJ2M19gRBbGttnRfApY3nQ3fdT8%3D@203.0.113.10:51820?"
const wgPeer = "publickey=biSnuvUOX2pmtzf249FK8UGSq2PQslGSdoj4UqH4yew%3D"
const vless = "vless://b831381d-6324-4d53-ad4f-8cda48b30811@203.0.113.10:443?security=tls&sni=a&encryption=mlkem768x25519plus.native.1rtt."
x25519 := strings.Repeat("A", 43)
// ss userinfo: "2022-blake3-aes-128-gcm:shortkey", "hello world" (method guessed), a 16-byte key
const ssShort = "ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206c2hvcnRrZXk@203.0.113.10:8388"
const ssGuessed = "ss://aGVsbG8gd29ybGQ@203.0.113.10:8388"
const ssValid = "ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206QndjSEJ3Y0hCd2NIQndjSEJ3Y0hCdz09@203.0.113.10:8388"
for _, tc := range []struct{ name, link, wantErr string }{
{"wireguard address", wg + wgPeer + "&address=garbage", `address entry "garbage"`},
{"wireguard no public key", wg + "address=10.0.0.2/32", "peer without publicKey"},
{"wireguard valid", wg + wgPeer + "&address=10.0.0.2/32", ""},
{"vless padding", vless + "abc." + x25519, "vless encryption"},
{"vless mlkem key", vless + strings.Repeat("_", 1579), "vless encryption"},
{"vless valid", vless + x25519, ""},
{"ss2022 short key", ssShort, "shadowsocks 2022 key"},
{"ss guessed 2022 method", ssGuessed, "shadowsocks 2022 key"},
{"ss2022 valid", ssValid, ""},
} {
t.Run(tc.name, func(t *testing.T) {
parsed, err := link.ParseLink(tc.link)
if err != nil {
t.Fatalf("ParseLink: %v", err)
}
parsed.Outbound["tag"] = "t"
raw, _ := json.Marshal(parsed.Outbound)
err = ValidateOutboundConfig(raw)
if tc.wantErr == "" {
if err != nil {
t.Fatalf("ValidateOutboundConfig(%s) = %v, want nil", raw, err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("ValidateOutboundConfig(%s) = %v, want an error containing %q", raw, err, tc.wantErr)
}
})
}
}