mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-17 08:40:59 +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": [
|
"tags": [
|
||||||
"Inbounds"
|
"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",
|
"operationId": "get_panel_api_inbounds_options",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ const SS_BLAKE3_CHACHA20 = '2022-blake3-chacha20-poly1305';
|
|||||||
|
|
||||||
export interface CapabilityProtocolSlice {
|
export interface CapabilityProtocolSlice {
|
||||||
protocol: string;
|
protocol: string;
|
||||||
|
settings?: { encryption?: string; decryption?: string };
|
||||||
streamSettings?: { network?: string; security?: string };
|
streamSettings?: { network?: string; security?: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CapabilityVlessSlice extends CapabilityProtocolSlice {
|
export interface CapabilityVlessSlice extends CapabilityProtocolSlice {
|
||||||
settings?: { clients?: { flow?: string }[] };
|
settings?: { encryption?: string; decryption?: string; clients?: { flow?: string }[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CapabilityShadowsocksSlice {
|
export interface CapabilityShadowsocksSlice extends CapabilityProtocolSlice {
|
||||||
protocol: string;
|
settings?: { encryption?: string; method?: string };
|
||||||
settings?: { method?: string };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canEnableTls(values: CapabilityProtocolSlice): boolean {
|
export function canEnableTls(values: CapabilityProtocolSlice): boolean {
|
||||||
@@ -39,11 +39,28 @@ export function canEnableReality(values: CapabilityProtocolSlice): boolean {
|
|||||||
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
|
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 {
|
export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean {
|
||||||
|
if (values.protocol !== 'vless') return false;
|
||||||
|
const network = values.streamSettings?.network;
|
||||||
const security = values.streamSettings?.security;
|
const security = values.streamSettings?.security;
|
||||||
if (security !== 'tls' && security !== 'reality') return false;
|
|
||||||
if (values.streamSettings?.network !== 'tcp') return false;
|
// Classic XTLS Vision: raw TCP carried over TLS or REALITY.
|
||||||
return values.protocol === 'vless';
|
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 {
|
export function canEnableStream(values: { protocol: string }): boolean {
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const sections: readonly Section[] = [
|
|||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/panel/api/inbounds/options',
|
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',
|
responseSchema: 'InboundOption',
|
||||||
responseSchemaArray: true,
|
responseSchemaArray: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -121,6 +121,10 @@ export function buildInboundInfo(dbInbound: DBInboundLike): InboundInfo {
|
|||||||
}),
|
}),
|
||||||
isVlessTlsFlow: canEnableTlsFlow({
|
isVlessTlsFlow: canEnableTlsFlow({
|
||||||
protocol: dbInbound.protocol,
|
protocol: dbInbound.protocol,
|
||||||
|
settings: {
|
||||||
|
encryption: settings.encryption as string | undefined,
|
||||||
|
decryption: settings.decryption as string | undefined,
|
||||||
|
},
|
||||||
streamSettings: { network, security },
|
streamSettings: { network, security },
|
||||||
}),
|
}),
|
||||||
host: readNetworkHost(stream, network),
|
host: readNetworkHost(stream, network),
|
||||||
|
|||||||
@@ -180,6 +180,35 @@ describe('protocol-capability helpers with raw coerced shapes', () => {
|
|||||||
streamSettings: { network: 'tcp', security: 'tls' },
|
streamSettings: { network: 'tcp', security: 'tls' },
|
||||||
})).toBe(false);
|
})).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', () => {
|
describe('getInboundClients with schema-shaped inbound', () => {
|
||||||
|
|||||||
@@ -445,6 +445,20 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||||||
return buildVmessLink(obj)
|
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 {
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
@@ -484,6 +498,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
params["security"] = "none"
|
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)
|
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 {
|
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 = ""
|
c.Flow = ""
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
|
|||||||
@@ -10,23 +10,31 @@ import (
|
|||||||
|
|
||||||
func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) {
|
func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) {
|
||||||
const vision = "xtls-rprx-vision"
|
const vision = "xtls-rprx-vision"
|
||||||
|
const enc = `{"encryption":"mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y"}`
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
protocol model.Protocol
|
protocol model.Protocol
|
||||||
streamSettings string
|
streamSettings string
|
||||||
|
settings string
|
||||||
wantFlow string
|
wantFlow string
|
||||||
}{
|
}{
|
||||||
{"vless tcp reality keeps flow", model.VLESS, `{"network":"tcp","security":"reality"}`, vision},
|
{"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 tcp tls keeps flow", model.VLESS, `{"network":"tcp","security":"tls"}`, "", vision},
|
||||||
{"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, ""},
|
{"vless ws tls clears flow", model.VLESS, `{"network":"ws","security":"tls"}`, "", ""},
|
||||||
{"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, ""},
|
{"vless grpc tls clears flow", model.VLESS, `{"network":"grpc","security":"tls"}`, "", ""},
|
||||||
{"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, ""},
|
{"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, "", ""},
|
||||||
{"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, ""},
|
{"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, "", ""},
|
||||||
{"empty stream clears flow", model.VLESS, "", ""},
|
{"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 {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
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)
|
got := clientWithInboundFlow(model.Client{Email: "x@example.com", Flow: vision}, ib)
|
||||||
if got.Flow != tc.wantFlow {
|
if got.Flow != tc.wantFlow {
|
||||||
t.Errorf("Flow = %q, want %q", 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,
|
Tag: r.Tag,
|
||||||
Protocol: r.Protocol,
|
Protocol: r.Protocol,
|
||||||
Port: r.Port,
|
Port: r.Port,
|
||||||
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings),
|
TlsFlowCapable: inboundCanEnableTlsFlow(r.Protocol, r.StreamSettings, r.Settings),
|
||||||
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
SsMethod: inboundShadowsocksMethod(r.Protocol, r.Settings),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func (s *InboundService) buildTargetClientFromSource(source model.Client, target
|
|||||||
case model.VLESS:
|
case model.VLESS:
|
||||||
target.ID = s.generateRandomCredential(targetProtocol)
|
target.ID = s.generateRandomCredential(targetProtocol)
|
||||||
if (flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443") &&
|
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
|
target.Flow = flow
|
||||||
}
|
}
|
||||||
case model.Trojan, model.Shadowsocks:
|
case model.Trojan, model.Shadowsocks:
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ func inboundShadowsocksMethod(protocol, settings string) string {
|
|||||||
return s.Method
|
return s.Method
|
||||||
}
|
}
|
||||||
|
|
||||||
// inboundCanEnableTlsFlow mirrors Inbound.canEnableTlsFlow() from the frontend:
|
// inboundCanEnableTlsFlow mirrors canEnableTlsFlow() from the frontend
|
||||||
// XTLS Vision is only valid for VLESS on TCP with tls or reality.
|
// (frontend/src/lib/xray/protocol-capabilities.ts). XTLS Vision is valid for
|
||||||
func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
// 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) {
|
if protocol != string(model.VLESS) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -38,15 +43,51 @@ func inboundCanEnableTlsFlow(protocol, streamSettings string) bool {
|
|||||||
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
|
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
|
||||||
return false
|
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 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.
|
// inboundCanHostFallbacks gates the settings.fallbacks injection.
|
||||||
// Xray only honors fallbacks on VLESS and Trojan inbounds carried over
|
// 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 {
|
func inboundCanHostFallbacks(ib *model.Inbound) bool {
|
||||||
if ib == nil {
|
if ib == nil {
|
||||||
return false
|
return false
|
||||||
@@ -54,13 +95,13 @@ func inboundCanHostFallbacks(ib *model.Inbound) bool {
|
|||||||
if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan {
|
if ib.Protocol != model.VLESS && ib.Protocol != model.Trojan {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return inboundCanEnableTlsFlow(string(ib.Protocol), ib.StreamSettings) ||
|
return streamSupportsFallbacks(ib.StreamSettings)
|
||||||
(ib.Protocol == model.Trojan && trojanStreamSupportsFallbacks(ib.StreamSettings))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trojanStreamSupportsFallbacks mirrors the Trojan side of the same gate
|
// streamSupportsFallbacks reports whether the stream is raw TCP carried over
|
||||||
// (Trojan reuses XTLS-Vision capable streams: tcp + tls or reality).
|
// TLS or REALITY — the only transport Xray honors inbound fallbacks on (and the
|
||||||
func trojanStreamSupportsFallbacks(streamSettings string) bool {
|
// classic requirement for XTLS Vision before vlessenc).
|
||||||
|
func streamSupportsFallbacks(streamSettings string) bool {
|
||||||
if streamSettings == "" {
|
if streamSettings == "" {
|
||||||
return false
|
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