mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
2d72d9c715
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.
153 lines
7.1 KiB
Go
153 lines
7.1 KiB
Go
package xray
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/link"
|
|
)
|
|
|
|
// TestValidateOutboundConfig_RejectsUnencryptedPublicVless covers xray-core
|
|
// v26.7.11's refusal to build an unencrypted vless outbound to a public
|
|
// address — the check now runs in-process, so the panel can surface it before
|
|
// a config reaches the core and bricks startup. A private-address outbound and
|
|
// a TLS outbound stay valid.
|
|
func TestValidateOutboundConfig_RejectsUnencryptedPublicVless(t *testing.T) {
|
|
publicPlaintext := `{
|
|
"protocol": "vless",
|
|
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
|
|
"streamSettings": {"network": "tcp", "security": "none"}
|
|
}`
|
|
if err := ValidateOutboundConfig([]byte(publicPlaintext)); err == nil {
|
|
t.Fatal("expected a public unencrypted vless outbound to be rejected")
|
|
} else if !strings.Contains(err.Error(), "prohibited") {
|
|
t.Fatalf("expected a prohibition error, got: %v", err)
|
|
}
|
|
|
|
privatePlaintext := `{
|
|
"protocol": "vless",
|
|
"settings": {"address": "10.0.0.1", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
|
|
"streamSettings": {"network": "tcp", "security": "none"}
|
|
}`
|
|
if err := ValidateOutboundConfig([]byte(privatePlaintext)); err != nil {
|
|
t.Fatalf("a private-address plaintext vless outbound must stay valid, got: %v", err)
|
|
}
|
|
|
|
publicTLS := `{
|
|
"protocol": "vless",
|
|
"settings": {"address": "1.2.3.4", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
|
|
"streamSettings": {"network": "tcp", "security": "tls", "tlsSettings": {"serverName": "example.com"}}
|
|
}`
|
|
if err := ValidateOutboundConfig([]byte(publicTLS)); err != nil {
|
|
t.Fatalf("a TLS-secured public vless outbound must stay valid, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// The core feeds remoteDNS to netip.MustParseAddr when it creates the outbound, so a
|
|
// non-IP entry ("local" until 26.9.30) panics it at startup; conf.Build() lets it through.
|
|
func TestValidateOutboundConfig_RejectsWireguardRemoteDNSThatIsNotAnIP(t *testing.T) {
|
|
outbound := func(remoteDNS string) []byte {
|
|
return []byte(`{
|
|
"protocol": "wireguard",
|
|
"settings": {
|
|
"secretKey": "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=",
|
|
"address": ["10.0.0.2/32"],
|
|
"peers": [{"publicKey": "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=", "endpoint": "162.159.192.1:2408"}],
|
|
"remoteDNS": ` + remoteDNS + `
|
|
}
|
|
}`)
|
|
}
|
|
for _, rejected := range []string{`["local"]`, `["1.1.1.1", "dns.google"]`} {
|
|
err := ValidateOutboundConfig(outbound(rejected))
|
|
if err == nil || !strings.Contains(err.Error(), "remoteDNS") {
|
|
t.Errorf("remoteDNS %s: want a remoteDNS refusal, got %v", rejected, err)
|
|
}
|
|
}
|
|
if err := ValidateOutboundConfig(outbound(`["1.1.1.1", "2606:4700:4700::1111"]`)); err != nil {
|
|
t.Fatalf("IP remoteDNS entries must stay valid, got: %v", err)
|
|
}
|
|
}
|
|
|
|
// xray-core 26.10.17 compiles a noise mask's Lua script inside Build (#7124), where the panel would
|
|
// look beside its own binary; the xray child finds a relative script in its bin folder.
|
|
func TestValidateOutboundConfig_FindsANoiseScriptBesideTheXrayBinary(t *testing.T) {
|
|
bin := t.TempDir()
|
|
t.Setenv("XUI_BIN_FOLDER", bin)
|
|
t.Chdir(t.TempDir())
|
|
if err := os.WriteFile(filepath.Join(bin, "noise.lua"), []byte("function HandleNoise(plan) end\n"), 0o600); err != nil {
|
|
t.Fatalf("write script: %v", err)
|
|
}
|
|
outbound := []byte(`{
|
|
"protocol": "vless",
|
|
"settings": {"address": "10.0.0.1", "port": 443, "id": "b831381d-6324-4d53-ad4f-8cda48b30811", "encryption": "none"},
|
|
"streamSettings": {"network": "tcp", "finalmask": {"udp": [{"type": "noise", "settings": {"script": "noise.lua"}}]}}
|
|
}`)
|
|
if err := ValidateOutboundConfig(outbound); err != nil {
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|