From a5a4c9cd831085f476126afdb792434c383a1647 Mon Sep 17 00:00:00 2001 From: BlindMaster24 <375291171150z@gmail.com> Date: Mon, 14 Sep 2026 16:59:34 +0300 Subject: [PATCH] fix(panel): read an outbound protocol id the way the core does (#6522) * fix(panel): read an outbound protocol id the way the core does xray-core lowercases a protocol id before it resolves the handler, so a template that spells the direct outbound "Freedom" is that outbound. The outbound editor fell through to the vless default and rendered it as an empty vless server, and the Basics tab did not find it and appended a second "direct", which the core refuses to load with "existing tag found". * fix(panel): never leave the direct tag on two outbounds A "direct" tag held by a non-freedom egress made both Basics-tab setters push a fresh freedom outbound, and the core refuses a config whose tags repeat ("existing tag found: direct"). The tag is now checked on its own before anything is added, matching setDefaultOutboundTag. * fix(panel): disable the freedom controls when direct is held elsewhere When a non-freedom outbound holds the "direct" tag, both Basics setters drop the edit so the core never sees the tag twice, but the Freedom Strategy select and the Happy Eyeballs switch stayed enabled and snapped back with no sign of why. isDirectTagTaken now disables both controls in that state. The find-or-create-plus-guard was also copied into BasicsTab's happy eyeballs setter with no test of its own; ensureDirectFreedomOutbound now owns the lookup, the guard and the creation for both setters, so the existing helper tests cover that path too. --------- Co-authored-by: Sanaei --- .../src/lib/xray/outbound-form-adapter.ts | 4 +- frontend/src/pages/xray/basics/BasicsTab.tsx | 24 +++++----- frontend/src/pages/xray/basics/helpers.ts | 38 +++++++++++----- .../src/test/basics-freedom-strategy.test.ts | 35 +++++++++++++++ .../src/test/basics-tab-direct-tag.test.tsx | 44 +++++++++++++++++++ .../src/test/outbound-form-adapter.test.ts | 26 +++++++++++ 6 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 frontend/src/test/basics-tab-direct-tag.test.tsx diff --git a/frontend/src/lib/xray/outbound-form-adapter.ts b/frontend/src/lib/xray/outbound-form-adapter.ts index 2c5a40b6a..a1ed479f5 100644 --- a/frontend/src/lib/xray/outbound-form-adapter.ts +++ b/frontend/src/lib/xray/outbound-form-adapter.ts @@ -541,7 +541,9 @@ function hydrateStreamForm(stream: Raw): OutboundStreamFormValues { } export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues { - const protocol = asString(raw.protocol, 'vless'); + // The core lowercases a protocol id before it looks the handler up, so a + // template pasted as "Freedom" must not fall through to the vless default. + const protocol = asString(raw.protocol, 'vless').toLowerCase(); const settings = asObject(raw.settings); const tag = asString(raw.tag); const sendThrough = asString(raw.sendThrough); diff --git a/frontend/src/pages/xray/basics/BasicsTab.tsx b/frontend/src/pages/xray/basics/BasicsTab.tsx index 068196591..2ae686512 100644 --- a/frontend/src/pages/xray/basics/BasicsTab.tsx +++ b/frontend/src/pages/xray/basics/BasicsTab.tsx @@ -25,7 +25,13 @@ import { MASK_ADDRESS, ROUTING_DOMAIN_STRATEGIES, } from './constants'; -import { directFreedomStrategy, setDirectFreedomStrategy } from './helpers'; +import { + directFreedomStrategy, + ensureDirectFreedomOutbound, + isDirectFreedomOutbound, + isDirectTagTaken, + setDirectFreedomStrategy, +} from './helpers'; interface BasicsTabProps { templateSettings: XraySettingsValue | null; @@ -112,9 +118,10 @@ export default function BasicsTab({ const freedomStrategy = directFreedomStrategy(templateSettings); - const directFreedomOutbound = templateSettings?.outbounds?.find( - (o) => o?.protocol === 'freedom' && o?.tag === 'direct', + const directFreedomOutbound = templateSettings?.outbounds?.find((o) => + isDirectFreedomOutbound(o), ); + const directTagTaken = isDirectTagTaken(templateSettings); const directHappyEyeballs = (() => { const sockopt = ( directFreedomOutbound?.streamSettings as { sockopt?: { happyEyeballs?: unknown } } | undefined @@ -128,13 +135,8 @@ export default function BasicsTab({ const setDirectHappyEyeballs = useCallback( (next: ReturnType | null) => { mutate((tt) => { - if (!tt.outbounds) tt.outbounds = []; - let idx = tt.outbounds.findIndex((o) => o?.protocol === 'freedom' && o?.tag === 'direct'); - if (idx < 0) { - tt.outbounds.push({ protocol: 'freedom', tag: 'direct', settings: {} }); - idx = tt.outbounds.length - 1; - } - const ob = tt.outbounds[idx]; + const ob = ensureDirectFreedomOutbound(tt); + if (!ob) return; const stream = (ob.streamSettings ?? {}) as Record; const sockopt = (stream.sockopt ?? {}) as Record; if (next == null) { @@ -181,6 +183,7 @@ export default function BasicsTab({ control={