Files
3x-ui/internal/xray/outbound_validation_test.go
T
MHSanaei 61b129d5b3 feat(xray): update xray-core to v26.10.21 and adapt panel
Bump xtls/xray-core to 1b1647cd9ddf (v26.10.21) and move the three
binary pins (DockerInit.sh, both release.yml fetches) in lockstep. The
v26.10.10 release was pulled upstream, so the old pins already 404.

XDRIVE (#7182): the core now runs XDRIVE only for VLESS inside VLESS
encryption, with no flow, and an outbound only over Mux; it refuses the
whole config otherwise. The panel offers XDRIVE only for VLESS, refuses
to save an inbound without decryption, never emits a client flow on an
XDRIVE stream (config, JSON subscription, forms), and turns Mux on when
an outbound picks XDRIVE. An inbound or template outbound stored before
the upgrade that the core would refuse is left out of the generated
config with a warning, so it no longer stops every other inbound. The
service "Google Drive" is now "google-drive" (old name read as a
deprecated alias); saves, the schema and both form adapters store the
new name. XDRIVE and MASQUE gain a client xmux; both schemas keep it
instead of stripping it on an inbound save.

Lua scripts (#7124, #7136): the core refuses a routing script beside
rules or domainStrategy, and a dns script beside the fallback/parallel
switches or a domain-scoped server. The panel always keeps its api
routing rule, so a template save now refuses any routing script and a
dns script in a refused combination, and a stored one is dropped from
the generated config so the core still starts. Noise masks gain a Lua
script that the loader compiles inside Build: the in-process build
(template save check, hot add) now resolves a relative script the way
the xray child does, beside the xray binary, instead of failing it. The
mask editor gets a Script field that replaces the noise list.

XDNS (#7140): a resolver takes https:// DoH addresses with worker,
tlsConfig and quicConfig. The mask editor registered only addrs, so a
save dropped the rest; it now edits all four.

Finalmask filter (#7206): a new TCP/UDP mask screens an inbound's source
IPs. The inbound mask editor offers it with its allow and block lists;
it is a no-op on a dialer and an older client core refuses the type, so
the share link and JSON subscription drop it like the Go link already
does.
2026-10-22 17:10:11 +02:00

89 lines
3.7 KiB
Go

package xray
import (
"os"
"path/filepath"
"strings"
"testing"
)
// 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)
}
}