Files
3x-ui/internal/sub/service_flow_test.go
T
MHSanaei c5e1e54c75 fix(xray): keep XDRIVE storage and Vision flow within what xray-core runs
xray-core opens an XDRIVE stream's storage only when the inbound starts
listening, so a Google Drive inbound saved with the form's defaults (no
folder, no secrets), a local one without a folder, or a template one
whose template xray cannot use stopped xray and every other inbound with
it. validateXDriveStorage applies the core's storage rules at save, on
inbound add/update and on the xray template's inbounds and outbounds:
the service must be local, Google Drive or template; local and Google
Drive need a remoteFolder; Google Drive needs its three non-empty
secrets; a template must pass newTemplateStorage's checks (put, get,
list and delete urls, and a list namesRegex with one capture group).

A VLESS client kept its xtls-rprx-vision flow when its inbound moved to
a transport that cannot carry it, and GetXrayConfig only honoured
DisableFlow, so the server demanded Vision there and refused every
client. GetXrayConfig and the outbound form now apply the Vision gate,
and that gate now follows the core in the backend, the subscription
service and the frontend: TLS/REALITY on raw TCP, or VLESS encryption on
any transport (xray runs Vision inside encryption.CommonConn whatever
the transport, not only on XHTTP). The outbound modal passes the VLESS
encryption into the gate and gates Mux on the effective flow.
2026-10-12 00:49:11 +02:00

112 lines
3.5 KiB
Go

package sub
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// Issue #5232: a vision flow set on a VLESS+XHTTP+REALITY (vlessenc) client
// must survive into subscription output, not just the inbound JSON.
const testMlkemEncryption = "mlkem768x25519plus.native.0rtt.dGVzdC1rZXk"
func TestVlessFlowAllowed(t *testing.T) {
enc := map[string]any{"encryption": testMlkemEncryption}
noEnc := map[string]any{"encryption": "none"}
tests := []struct {
name string
network string
security string
settings map[string]any
want bool
}{
{"tcp tls", "tcp", "tls", noEnc, true},
{"tcp reality", "tcp", "reality", noEnc, true},
{"tcp none", "tcp", "none", noEnc, false},
{"tcp none vlessenc", "tcp", "none", enc, true},
{"ws none vlessenc", "ws", "none", enc, true},
{"xhttp none vlessenc", "xhttp", "none", enc, true},
{"xhttp reality vlessenc (#5232)", "xhttp", "reality", enc, true},
{"xhttp tls vlessenc", "xhttp", "tls", enc, true},
{"xhttp reality no vlessenc", "xhttp", "reality", noEnc, false},
{"ws tls", "ws", "tls", noEnc, false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := vlessFlowAllowed(tc.network, tc.security, tc.settings); got != tc.want {
t.Fatalf("vlessFlowAllowed(%q, %q, %v) = %v, want %v", tc.network, tc.security, tc.settings, got, tc.want)
}
})
}
}
func flowTestInbound(streamSettings, encryption string) *model.Inbound {
return &model.Inbound{
Listen: "203.0.113.1",
Port: 443,
Protocol: model.VLESS,
Remark: "flowtest",
Settings: `{"clients":[{"id":"11111111-2222-4333-8444-555555555555","email":"user","flow":"xtls-rprx-vision"}],` +
`"decryption":"` + encryption + `","encryption":"` + encryption + `"}`,
StreamSettings: streamSettings,
}
}
const xhttpRealityStream = `{
"network": "xhttp",
"security": "reality",
"xhttpSettings": {"path": "/", "mode": "auto"},
"realitySettings": {
"serverNames": ["example.com"],
"shortIds": ["abcd"],
"settings": {"publicKey": "pub", "fingerprint": "chrome"}
}
}`
func TestGenVlessLink_FlowXhttpRealityVlessenc(t *testing.T) {
s := &SubService{}
link := s.genVlessLink(flowTestInbound(xhttpRealityStream, testMlkemEncryption), "user")
if !strings.Contains(link, "flow=xtls-rprx-vision") {
t.Fatalf("xhttp+reality+vlessenc link must carry the vision flow (#5232), got %q", link)
}
}
func TestGenVlessLink_NoFlowXhttpRealityWithoutVlessenc(t *testing.T) {
s := &SubService{}
link := s.genVlessLink(flowTestInbound(xhttpRealityStream, "none"), "user")
if strings.Contains(link, "flow=") {
t.Fatalf("xhttp+reality without vlessenc must not carry a flow, got %q", link)
}
}
func TestGenVlessLink_DisableFlowSuppressesFlow(t *testing.T) {
s := &SubService{}
ib := flowTestInbound(xhttpRealityStream, testMlkemEncryption)
ib.DisableFlow = true
link := s.genVlessLink(ib, "user")
if strings.Contains(link, "flow=") {
t.Fatalf("DisableFlow inbound must not carry a flow even when the transport is capable, got %q", link)
}
}
func TestGenVlessLink_FlowTcpRealityStillWorks(t *testing.T) {
stream := `{
"network": "tcp",
"security": "reality",
"tcpSettings": {"header": {"type": "none"}},
"realitySettings": {
"serverNames": ["example.com"],
"shortIds": ["abcd"],
"settings": {"publicKey": "pub", "fingerprint": "chrome"}
}
}`
s := &SubService{}
link := s.genVlessLink(flowTestInbound(stream, "none"), "user")
if !strings.Contains(link, "flow=xtls-rprx-vision") {
t.Fatalf("tcp+reality link must keep the vision flow, got %q", link)
}
}