mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
* fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157) * fix: enable xtls-rprx-vision flow for VLESS XHTTP with vlessenc encryption (#5157) The flow selector was hidden and the vless:// link omitted flow= because: 1. The backend gate (inboundCanEnableTlsFlow) only accepted tcp+tls/reality. 2. The PR #5185 frontend check used `encryption === 'vlessenc'`, which never matches — the stored value is a generated ML-KEM dotted string, not the CLI subcommand name. Fix: extend inboundCanEnableTlsFlow to also return true for XHTTP when a non-none vlessenc encryption/decryption value is present. Update all three call-sites (inbound.go TlsFlowCapable field, client_crud.go clientWithInboundFlow, inbound_clients.go copy-flow path) and the sub/service.go link generator. Scope is XHTTP-only: TCP without tls/reality is intentionally excluded. Add inbound_protocol_test.go covering the new and existing gate combinations, extend client_flow_isolation_test.go with xhttp+vlessenc cases, and add frontend tests for canEnableTlsFlow with real ML-KEM key values. --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
@@ -2185,7 +2185,7 @@
|
||||
"tags": [
|
||||
"Inbounds"
|
||||
],
|
||||
"summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.",
|
||||
"summary": "Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.",
|
||||
"operationId": "get_panel_api_inbounds_options",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
||||
@@ -16,16 +16,16 @@ const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
|
||||
|
||||
export interface CapabilityProtocolSlice {
|
||||
protocol: string;
|
||||
settings?: { encryption?: string; decryption?: string };
|
||||
streamSettings?: { network?: string; security?: string };
|
||||
}
|
||||
|
||||
export interface CapabilityVlessSlice extends CapabilityProtocolSlice {
|
||||
settings?: { clients?: { flow?: string }[] };
|
||||
settings?: { encryption?: string; decryption?: string; clients?: { flow?: string }[] };
|
||||
}
|
||||
|
||||
export interface CapabilityShadowsocksSlice {
|
||||
protocol: string;
|
||||
settings?: { method?: string };
|
||||
export interface CapabilityShadowsocksSlice extends CapabilityProtocolSlice {
|
||||
settings?: { encryption?: string; method?: string };
|
||||
}
|
||||
|
||||
export function canEnableTls(values: CapabilityProtocolSlice): boolean {
|
||||
@@ -39,11 +39,28 @@ export function canEnableReality(values: CapabilityProtocolSlice): boolean {
|
||||
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
|
||||
}
|
||||
|
||||
// VLESS encryption (vlessenc / ML-KEM) is on when encryption or decryption holds
|
||||
// a generated value (e.g. "mlkem768x25519plus.native.0rtt.<key>") rather than
|
||||
// the "none"/"" sentinel. The value is never the literal "vlessenc" (that is the
|
||||
// `xray vlessenc` subcommand). decryption is the server-side value; encryption is
|
||||
// stored for link generation — either being set means it is on.
|
||||
function hasVlessEncryption(settings: CapabilityProtocolSlice['settings']): boolean {
|
||||
const isSet = (v?: string) => v != null && v !== '' && v !== 'none';
|
||||
return isSet(settings?.encryption) || isSet(settings?.decryption);
|
||||
}
|
||||
|
||||
export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean {
|
||||
if (values.protocol !== 'vless') return false;
|
||||
const network = values.streamSettings?.network;
|
||||
const security = values.streamSettings?.security;
|
||||
if (security !== 'tls' && security !== 'reality') return false;
|
||||
if (values.streamSettings?.network !== 'tcp') return false;
|
||||
return values.protocol === 'vless';
|
||||
|
||||
// Classic XTLS Vision: raw TCP carried over TLS or REALITY.
|
||||
if (network === 'tcp' && (security === 'tls' || security === 'reality')) return true;
|
||||
|
||||
// vlessenc carries Vision over XHTTP without transport TLS.
|
||||
if (network === 'xhttp' && hasVlessEncryption(values.settings)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function canEnableStream(values: { protocol: string }): boolean {
|
||||
|
||||
@@ -122,7 +122,7 @@ export const sections: readonly Section[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/inbounds/options',
|
||||
summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS / port-fallback on TCP with tls or reality), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.',
|
||||
summary: 'Lightweight picker projection of the authenticated user’s inbounds. Returns id, remark, tag, protocol, port, a server-computed tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the client UI to generate a valid Shadowsocks 2022 PSK). Use this for dropdowns and attach pickers — it skips settings, streamSettings, and clientStats so the payload stays small even on panels with thousands of clients.',
|
||||
responseSchema: 'InboundOption',
|
||||
responseSchemaArray: true,
|
||||
},
|
||||
|
||||
@@ -121,6 +121,10 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
||||
}),
|
||||
isVlessTlsFlow: canEnableTlsFlow({
|
||||
protocol: dbInbound.protocol,
|
||||
settings: {
|
||||
encryption: settings.encryption as string | undefined,
|
||||
decryption: settings.decryption as string | undefined,
|
||||
},
|
||||
streamSettings: { network, security },
|
||||
}),
|
||||
host: readNetworkHost(stream, network),
|
||||
|
||||
@@ -180,6 +180,35 @@ describe('protocol-capability helpers with raw coerced shapes', () => {
|
||||
streamSettings: { network: 'tcp', security: 'tls' },
|
||||
})).toBe(false);
|
||||
});
|
||||
|
||||
it('canEnableTlsFlow allows vless + xhttp when vlessenc encryption is set', () => {
|
||||
const enc = 'mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y';
|
||||
const dec = 'mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E';
|
||||
// XHTTP + a real (generated) encryption value → Vision flow allowed.
|
||||
expect(canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
settings: { encryption: enc },
|
||||
streamSettings: { network: 'xhttp', security: 'none' },
|
||||
})).toBe(true);
|
||||
// decryption alone (server-side value) is enough on XHTTP.
|
||||
expect(canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
settings: { decryption: dec, encryption: 'none' },
|
||||
streamSettings: { network: 'xhttp', security: 'none' },
|
||||
})).toBe(true);
|
||||
// No encryption → stays gated off.
|
||||
expect(canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
settings: { encryption: 'none' },
|
||||
streamSettings: { network: 'xhttp', security: 'none' },
|
||||
})).toBe(false);
|
||||
// vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable.
|
||||
expect(canEnableTlsFlow({
|
||||
protocol: 'vless',
|
||||
settings: { decryption: dec, encryption: enc },
|
||||
streamSettings: { network: 'tcp', security: 'none' },
|
||||
})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboundClients with schema-shaped inbound', () => {
|
||||
|
||||
@@ -445,6 +445,20 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
return buildVmessLink(obj)
|
||||
}
|
||||
|
||||
// vlessEncryptionEnabled reports whether the VLESS inbound settings enable
|
||||
// VLESS-level encryption (vlessenc / ML-KEM). When on, the encryption/decryption
|
||||
// fields hold a generated dotted string (e.g. "mlkem768x25519plus.native.0rtt.<key>");
|
||||
// "none" or empty means off. The value is never the literal "vlessenc" — that is
|
||||
// the `xray vlessenc` CLI subcommand name, not a stored value.
|
||||
func vlessEncryptionEnabled(settings map[string]any) bool {
|
||||
for _, key := range []string{"encryption", "decryption"} {
|
||||
if v, ok := settings[key].(string); ok && v != "" && v != "none" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
@@ -484,6 +498,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
default:
|
||||
params["security"] = "none"
|
||||
// VLESS encryption (vlessenc / ML-KEM) carries XTLS Vision over XHTTP
|
||||
// without transport TLS.
|
||||
if streamNetwork == "xhttp" && len(clients[clientIndex].Flow) > 0 && vlessEncryptionEnabled(settings) {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]any)
|
||||
|
||||
@@ -146,7 +146,7 @@ func (s *ClientService) fillProtocolDefaults(c *model.Client, ib *model.Inbound)
|
||||
}
|
||||
|
||||
func clientWithInboundFlow(c model.Client, ib *model.Inbound) model.Client {
|
||||
if !inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) {
|
||||
if !inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings, ib.Settings) {
|
||||
c.Flow = ""
|
||||
}
|
||||
return c
|
||||
|
||||
@@ -10,23 +10,31 @@ import (
|
||||
|
||||
func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) {
|
||||
const vision = "xtls-rprx-vision"
|
||||
const enc = `{"encryption":"mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y"}`
|
||||
cases := []struct {
|
||||
name string
|
||||
protocol model.Protocol
|
||||
streamSettings string
|
||||
settings string
|
||||
wantFlow string
|
||||
}{
|
||||
{"vless tcp reality keeps flow", model.VLESS, `{"network":"tcp","security":"reality"}`, vision},
|
||||
{"vless tcp tls keeps flow", model.VLESS, `{"network":"tcp","security":"tls"}`, vision},
|
||||
{"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, ""},
|
||||
{"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, ""},
|
||||
{"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, ""},
|
||||
{"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, ""},
|
||||
{"empty stream clears flow", model.VLESS, "", ""},
|
||||
{"vless tcp reality keeps flow", model.VLESS, `{"network":"tcp","security":"reality"}`, "", vision},
|
||||
{"vless tcp tls keeps flow", model.VLESS, `{"network":"tcp","security":"tls"}`, "", vision},
|
||||
{"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, "", ""},
|
||||
{"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, "", ""},
|
||||
{"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, "", ""},
|
||||
{"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, "", ""},
|
||||
{"empty stream clears flow", model.VLESS, "", "", ""},
|
||||
// vlessenc (ML-KEM) keeps Vision flow without transport TLS only on XHTTP.
|
||||
// TCP without tls/reality clears it even with vlessenc set.
|
||||
{"vless tcp vlessenc clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, enc, ""},
|
||||
{"vless xhttp vlessenc keeps flow", model.VLESS, `{"network":"xhttp","security":"none"}`, enc, vision},
|
||||
{"vless xhttp no encryption clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, ""},
|
||||
{"vless xhttp empty settings clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, "", ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings}
|
||||
ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings, Settings: tc.settings}
|
||||
got := clientWithInboundFlow(model.Client{Email: "x@example.com", Flow: vision}, ib)
|
||||
if got.Flow != tc.wantFlow {
|
||||
t.Errorf("Flow = %q, want %q", got.Flow, tc.wantFlow)
|
||||
|
||||
@@ -196,7 +196,7 @@ func (s *InboundService) GetInboundOptions(userId int) ([]InboundOption, error)
|
||||
Tag: r.Tag,
|
||||
Protocol: r.Protocol,
|
||||
Port: r.Port,
|
||||
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings),
|
||||
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
|
||||
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target
|
||||
case model.VLESS:
|
||||
target.ID = s.generateRandomCredential(targetProtocol)
|
||||
if (flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443") &&
|
||||
inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings) {
|
||||
inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings, targetInbound.Settings) {
|
||||
target.Flow = flow
|
||||
}
|
||||
case model.Trojan, model.Shadowsocks:
|
||||
|
||||
@@ -22,9 +22,14 @@ func inboundShadowsocksMethod(protocol, settings string) string {
|
||||
return s.Method
|
||||
}
|
||||
|
||||
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend:
|
||||
// XTLS Vision is only valid for VLESS on TCP with tls or reality.
|
||||
func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
||||
// inboundCanEnableTlsFlow mirrors canEnableTlsFlow() from the frontend
|
||||
// (frontend/src/lib/xray/protocol-capabilities.ts). XTLS Vision is valid for
|
||||
// VLESS on TCP with tls or reality (classic), and on XHTTP when VLESS encryption
|
||||
// (vlessenc / ML-KEM) is enabled — there the post-quantum, VLESS-level
|
||||
// encryption stands in for the transport TLS that Vision relies on. settings is
|
||||
// the inbound's raw settings JSON, which carries the encryption value
|
||||
// (streamSettings does not).
|
||||
func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool {
|
||||
if protocol != string(model.VLESS) {
|
||||
return false
|
||||
}
|
||||
@@ -38,15 +43,51 @@ func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
||||
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
|
||||
return false
|
||||
}
|
||||
if stream.Network != "tcp" {
|
||||
switch stream.Network {
|
||||
case "tcp":
|
||||
return stream.Security == "tls" || stream.Security == "reality"
|
||||
case "xhttp":
|
||||
return vlessEncryptionEnabled(settings)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return stream.Security == "tls" || stream.Security == "reality"
|
||||
}
|
||||
|
||||
// vlessEncryptionEnabled reports whether a VLESS inbound has VLESS-level
|
||||
// encryption (vlessenc / ML-KEM) configured. When enabled these fields hold a
|
||||
// generated dotted string (e.g. "mlkem768x25519plus.native.0rtt.<key>"); "none"
|
||||
// or empty means off. The value is never the literal "vlessenc" — that is the
|
||||
// name of the `xray vlessenc` CLI subcommand, not a stored value.
|
||||
//
|
||||
// Both fields are checked: decryption is the authoritative server-side value
|
||||
// xray-core reads, while encryption is stored by the panel for link generation.
|
||||
// The ML-KEM/X25519 buttons set both, but accepting either keeps the gate
|
||||
// working for inbounds configured via the API or raw JSON.
|
||||
func vlessEncryptionEnabled(settings string) bool {
|
||||
if settings == "" {
|
||||
return false
|
||||
}
|
||||
var s struct {
|
||||
Encryption string `json:"encryption"`
|
||||
Decryption string `json:"decryption"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(settings), &s); err != nil {
|
||||
return false
|
||||
}
|
||||
return vlessEncValueSet(s.Encryption) || vlessEncValueSet(s.Decryption)
|
||||
}
|
||||
|
||||
// vlessEncValueSet reports whether a VLESS encryption/decryption field holds a
|
||||
// real (generated) value rather than the "none"/empty sentinel.
|
||||
func vlessEncValueSet(v string) bool {
|
||||
return v != "" && v != "none"
|
||||
}
|
||||
|
||||
// inboundCanHostFallbacks gates the settings.fallbacks injection.
|
||||
// Xray only honors fallbacks on VLESS and Trojan inbounds carried over
|
||||
// TCP transport with TLS or Reality security.
|
||||
// TCP transport with TLS or Reality security. This is intentionally stricter
|
||||
// than inboundCanEnableTlsFlow (which also accepts XHTTP+vlessenc): fallbacks
|
||||
// are a raw-TCP-only feature.
|
||||
func inboundCanHostFallbacks(ib *model.Inbound) bool {
|
||||
if ib == nil {
|
||||
return false
|
||||
@@ -54,13 +95,13 @@ func inboundCanHostFallbacks(ib *model.Inbound) bool {
|
||||
if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan {
|
||||
return false
|
||||
}
|
||||
return inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) ||
|
||||
(ib.Protocol == model.Trojan && trojanStreamSupportsFallbacks(ib.StreamSettings))
|
||||
return streamSupportsFallbacks(ib.StreamSettings)
|
||||
}
|
||||
|
||||
// trojanStreamSupportsFallbacks mirrors the Trojan side of the same gate
|
||||
// (Trojan reuses XTLS-Vision capable streams: tcp + tls or reality).
|
||||
func trojanStreamSupportsFallbacks(streamSettings string) bool {
|
||||
// streamSupportsFallbacks reports whether the stream is raw TCP carried over
|
||||
// TLS or REALITY — the only transport Xray honors inbound fallbacks on (and the
|
||||
// classic requirement for XTLS Vision before vlessenc).
|
||||
func streamSupportsFallbacks(streamSettings string) bool {
|
||||
if streamSettings == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// A representative vlessenc/ML-KEM encryption value as produced by `xray
|
||||
// vlessenc` — a dotted string, never the literal "vlessenc".
|
||||
const vlessEncValue = "mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y"
|
||||
|
||||
func TestInboundCanEnableTlsFlow(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
protocol string
|
||||
streamSettings string
|
||||
settings string
|
||||
want bool
|
||||
}{
|
||||
{"vless tcp tls", string(model.VLESS), `{"network":"tcp","security":"tls"}`, "", true},
|
||||
{"vless tcp reality", string(model.VLESS), `{"network":"tcp","security":"reality"}`, "", true},
|
||||
{"vless tcp none no enc", string(model.VLESS), `{"network":"tcp","security":"none"}`, "", false},
|
||||
{"vless ws tls", string(model.VLESS), `{"network":"ws","security":"tls"}`, "", false},
|
||||
{"vless grpc reality", string(model.VLESS), `{"network":"grpc","security":"reality"}`, "", false},
|
||||
{"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false},
|
||||
{"empty stream", string(model.VLESS), "", "", false},
|
||||
|
||||
// vlessenc is gated to XHTTP only. TCP without tls/reality is NOT
|
||||
// Vision-capable even with vlessenc set — the combination only works on
|
||||
// XHTTP in practice.
|
||||
{"vless tcp vlessenc not capable", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, false},
|
||||
// ws is a framed transport — vlessenc never enables Vision there.
|
||||
{"vless ws vlessenc still off", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false},
|
||||
|
||||
// XHTTP + VLESS encryption (the #5157 case).
|
||||
{"vless xhttp vlessenc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true},
|
||||
{"vless xhttp encryption none", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, false},
|
||||
{"vless xhttp no settings", string(model.VLESS), `{"network":"xhttp","security":"none"}`, "", false},
|
||||
// Regression for PR #5185: the gate is "any non-none encryption", NOT an
|
||||
// equality check against the literal "vlessenc" (which the buggy PR used
|
||||
// and which never matches a real, generated encryption value). An x25519
|
||||
// auth value must enable it just like the ML-KEM value above.
|
||||
{"vless xhttp x25519 enc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"native.0rtt.121s-180s.xRMUYYjQctqYO1pSyffM-w"}`, true},
|
||||
// Server-side configs (API/JSON) may carry only decryption; that alone
|
||||
// must also enable the flow gate.
|
||||
{"vless xhttp decryption only", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"decryption":"` + vlessEncValue + `","encryption":"none"}`, true},
|
||||
// XHTTP without encryption stays off even with tls (Vision over XHTTP is
|
||||
// gated on vlessenc, not transport security).
|
||||
{"vless xhttp tls no encryption", string(model.VLESS), `{"network":"xhttp","security":"tls"}`, `{"encryption":"none"}`, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := inboundCanEnableTlsFlow(tc.protocol, tc.streamSettings, tc.settings)
|
||||
if got != tc.want {
|
||||
t.Errorf("inboundCanEnableTlsFlow(%q, %q, %q) = %v, want %v",
|
||||
tc.protocol, tc.streamSettings, tc.settings, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks must remain raw-TCP-only and must NOT follow the broadened flow gate
|
||||
// onto XHTTP+vlessenc.
|
||||
func TestInboundCanHostFallbacks_StaysTcpOnly(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
protocol model.Protocol
|
||||
streamSettings string
|
||||
settings string
|
||||
want bool
|
||||
}{
|
||||
{"vless tcp tls", model.VLESS, `{"network":"tcp","security":"tls"}`, "", true},
|
||||
{"trojan tcp reality", model.Trojan, `{"network":"tcp","security":"reality"}`, "", true},
|
||||
{"vless xhttp vlessenc not fallback-capable", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false},
|
||||
{"vmess tcp tls not fallback-capable", model.VMESS, `{"network":"tcp","security":"tls"}`, "", false},
|
||||
{"nil-ish empty stream", model.VLESS, "", "", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ib := &model.Inbound{Protocol: tc.protocol, StreamSettings: tc.streamSettings, Settings: tc.settings}
|
||||
if got := inboundCanHostFallbacks(ib); got != tc.want {
|
||||
t.Errorf("inboundCanHostFallbacks = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if inboundCanHostFallbacks(nil) {
|
||||
t.Errorf("inboundCanHostFallbacks(nil) = true, want false")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user