mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-29 06:27:14 +00:00
fix(sub): deliver vision flow for VLESS+XHTTP+REALITY in share links and Clash (#5232)
The vlessenc fix (#5185) enabled flow on XHTTP only in the security=none branch of genVlessLink, and the Clash builder still gated flow on network==tcp. With XHTTP+REALITY+vlessenc the panel accepts and stores the flow (inboundCanEnableTlsFlow passes), but subscriptions dropped it, so clients received configs without xtls-rprx-vision. Add vlessFlowAllowed mirroring inboundCanEnableTlsFlow — tcp with tls/reality, or xhttp with vlessenc regardless of security layer — and use it in both the vless:// link generator and the Clash proxy builder.
This commit is contained in:
@@ -208,11 +208,12 @@ func (s *SubClashService) buildProxy(inbound *model.Inbound, client model.Client
|
|||||||
case model.VLESS:
|
case model.VLESS:
|
||||||
proxy["type"] = "vless"
|
proxy["type"] = "vless"
|
||||||
proxy["uuid"] = client.ID
|
proxy["uuid"] = client.ID
|
||||||
if client.Flow != "" && network == "tcp" {
|
|
||||||
proxy["flow"] = client.Flow
|
|
||||||
}
|
|
||||||
var inboundSettings map[string]any
|
var inboundSettings map[string]any
|
||||||
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
|
||||||
|
streamSecurity, _ := stream["security"].(string)
|
||||||
|
if client.Flow != "" && vlessFlowAllowed(network, streamSecurity, inboundSettings) {
|
||||||
|
proxy["flow"] = client.Flow
|
||||||
|
}
|
||||||
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
if encryption, ok := inboundSettings["encryption"].(string); ok {
|
||||||
encryption = strings.TrimSpace(encryption)
|
encryption = strings.TrimSpace(encryption)
|
||||||
if encryption != "" && encryption != "none" {
|
if encryption != "" && encryption != "none" {
|
||||||
|
|||||||
@@ -148,6 +148,63 @@ func TestBuildProxy_VLESSPostQuantumEncryptionUsesMihomoEncryptionField(t *testi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildProxy_VLESSFlowXhttpRealityVlessenc(t *testing.T) {
|
||||||
|
svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
|
||||||
|
encryption := "mlkem768x25519plus.native.0rtt.client"
|
||||||
|
inbound := &model.Inbound{
|
||||||
|
Listen: "203.0.113.1",
|
||||||
|
Port: 443,
|
||||||
|
Protocol: model.VLESS,
|
||||||
|
Remark: "pq-flow",
|
||||||
|
Settings: `{"encryption":"` + encryption + `"}`,
|
||||||
|
}
|
||||||
|
client := model.Client{ID: "11111111-2222-4333-8444-555555555555", Flow: "xtls-rprx-vision"}
|
||||||
|
stream := map[string]any{
|
||||||
|
"network": "xhttp",
|
||||||
|
"xhttpSettings": map[string]any{
|
||||||
|
"path": "/",
|
||||||
|
"mode": "auto",
|
||||||
|
},
|
||||||
|
"security": "reality",
|
||||||
|
"realitySettings": map[string]any{
|
||||||
|
"publicKey": "pub",
|
||||||
|
"serverName": "example.com",
|
||||||
|
"shortId": "abcd",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := svc.buildProxy(inbound, client, stream, "")
|
||||||
|
|
||||||
|
if proxy["flow"] != "xtls-rprx-vision" {
|
||||||
|
t.Fatalf("xhttp+reality+vlessenc Clash proxy must carry the vision flow (#5232): %#v", proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildProxy_VLESSFlowDroppedWithoutVisionSupport(t *testing.T) {
|
||||||
|
svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
|
||||||
|
inbound := &model.Inbound{
|
||||||
|
Listen: "203.0.113.1",
|
||||||
|
Port: 443,
|
||||||
|
Protocol: model.VLESS,
|
||||||
|
Remark: "plain-flow",
|
||||||
|
Settings: `{"encryption":"none"}`,
|
||||||
|
}
|
||||||
|
client := model.Client{ID: "11111111-2222-4333-8444-555555555555", Flow: "xtls-rprx-vision"}
|
||||||
|
stream := map[string]any{
|
||||||
|
"network": "tcp",
|
||||||
|
"security": "none",
|
||||||
|
"tcpSettings": map[string]any{
|
||||||
|
"header": map[string]any{"type": "none"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := svc.buildProxy(inbound, client, stream, "")
|
||||||
|
|
||||||
|
if _, ok := proxy["flow"]; ok {
|
||||||
|
t.Fatalf("tcp without tls/reality must not carry a flow: %#v", proxy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildProxy_VLESSNoneEncryptionOmittedForClash(t *testing.T) {
|
func TestBuildProxy_VLESSNoneEncryptionOmittedForClash(t *testing.T) {
|
||||||
svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
|
svc := &SubClashService{SubService: &SubService{remarkModel: "-i"}}
|
||||||
inbound := &model.Inbound{
|
inbound := &model.Inbound{
|
||||||
|
|||||||
+20
-11
@@ -484,6 +484,23 @@ func vlessEncryptionEnabled(settings map[string]any) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// vlessFlowAllowed reports whether a client's XTLS Vision flow belongs in
|
||||||
|
// generated links/configs. Mirrors inboundCanEnableTlsFlow in
|
||||||
|
// internal/web/service: Vision runs on TCP with tls/reality (classic), and on
|
||||||
|
// XHTTP whenever VLESS encryption (vlessenc / ML-KEM) is enabled — there the
|
||||||
|
// VLESS-level encryption stands in for the transport TLS that Vision relies
|
||||||
|
// on, regardless of the stream security layer (so XHTTP+REALITY+vlessenc
|
||||||
|
// keeps its flow too).
|
||||||
|
func vlessFlowAllowed(network, security string, settings map[string]any) bool {
|
||||||
|
switch network {
|
||||||
|
case "tcp":
|
||||||
|
return security == "tls" || security == "reality"
|
||||||
|
case "xhttp":
|
||||||
|
return vlessEncryptionEnabled(settings)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
@@ -513,21 +530,13 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
switch security {
|
switch security {
|
||||||
case "tls":
|
case "tls":
|
||||||
applyShareTLSParams(stream, params)
|
applyShareTLSParams(stream, params)
|
||||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
|
||||||
params["flow"] = clients[clientIndex].Flow
|
|
||||||
}
|
|
||||||
case "reality":
|
case "reality":
|
||||||
applyShareRealityParams(stream, params)
|
applyShareRealityParams(stream, params)
|
||||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
|
||||||
params["flow"] = clients[clientIndex].Flow
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
params["security"] = "none"
|
params["security"] = "none"
|
||||||
// VLESS encryption (vlessenc / ML-KEM) carries XTLS Vision over XHTTP
|
}
|
||||||
// without transport TLS.
|
if len(clients[clientIndex].Flow) > 0 && vlessFlowAllowed(streamNetwork, security, settings) {
|
||||||
if streamNetwork == "xhttp" && len(clients[clientIndex].Flow) > 0 && vlessEncryptionEnabled(settings) {
|
params["flow"] = clients[clientIndex].Flow
|
||||||
params["flow"] = clients[clientIndex].Flow
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
externalProxies, _ := stream["externalProxy"].([]any)
|
externalProxies, _ := stream["externalProxy"].([]any)
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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, false},
|
||||||
|
{"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{remarkModel: "-ieo"}
|
||||||
|
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{remarkModel: "-ieo"}
|
||||||
|
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_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{remarkModel: "-ieo"}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user