From 79a7e7a5b5c1590c4997c08540cbce12e2bf272d Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Thu, 7 May 2026 14:44:33 +0200 Subject: [PATCH] fix(vless): scope testseed to xtls-rprx-vision flow testseed is only meaningful for the exact xtls-rprx-vision flow, but the panel was emitting it for any non-empty flow (including the UDP variant) and keeping it on the inbound after the flow was cleared via the client modal. Tighten the gate end-to-end: - VLESSSettings.toJson (inbound + outbound) now only emits testseed when the flow is exactly xtls-rprx-vision and the array is 4 positive ints; default state is empty so unmodified inbounds omit the field entirely. - canEnableVisionSeed drops the udp443 variant per spec. - Form adds a tooltip + theme-aware help text and an inline error when the user partially fills the four inputs; submit is blocked in that state. Reset clears to empty (= use server defaults). - UpdateInboundClient strips a now-orphaned testseed when the spliced client no longer leaves any XRV flow in the inbound. - MigrationRequirements cleans up legacy rows where testseed lingered after flow changes or was saved for non-XRV flows by older versions. Co-Authored-By: Claude Opus 4.7 --- web/assets/js/model/inbound.js | 41 +++++--- web/assets/js/model/outbound.js | 24 +++-- web/html/form/protocol/vless.html | 30 ++++-- web/html/modals/inbound_modal.html | 145 ++++++++++++++--------------- web/service/inbound.go | 35 +++++++ 5 files changed, 173 insertions(+), 102 deletions(-) diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 263091d9..074670a9 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -1763,12 +1763,13 @@ class Inbound extends XrayCommonClass { return false; } - // Vision seed applies only when vision flow is selected + // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected. + // Excludes the UDP variant per spec. canEnableVisionSeed() { if (!this.canEnableTlsFlow()) return false; const clients = this.settings?.vlesses; if (!Array.isArray(clients)) return false; - return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION || c?.flow === TLS_FLOW_CONTROL.VISION_UDP443); + return clients.some(c => c?.flow === TLS_FLOW_CONTROL.VISION); } canEnableReality() { @@ -2543,7 +2544,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings { encryption = "none", fallbacks = [], selectedAuth = undefined, - testseed = [900, 500, 900, 256], + testseed = [], ) { super(protocol); this.vlesses = vlesses; @@ -2562,12 +2563,23 @@ Inbound.VLESSSettings = class extends Inbound.Settings { this.fallbacks.splice(index, 1); } + // Empty array means "use server defaults" (won't be sent). + // Anything else must be exactly 4 positive integers. + static isValidTestseed(arr) { + if (!Array.isArray(arr) || arr.length === 0) return true; + if (arr.length !== 4) return false; + return arr.every(v => Number.isInteger(v) && v > 0); + } + static fromJson(json = {}) { - // Ensure testseed is always initialized as an array - let testseed = [900, 500, 900, 256]; - if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) { - testseed = json.testseed; - } + // Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty + // so toJson omits it and the form falls back to placeholder defaults. + const saved = json.testseed; + const testseed = (Array.isArray(saved) + && saved.length === 4 + && saved.every(v => Number.isInteger(v) && v > 0)) + ? saved + : []; const obj = new Inbound.VLESSSettings( Protocols.VLESS, @@ -2576,7 +2588,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings { json.encryption, Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []), json.selectedAuth, - testseed + testseed, ); return obj; } @@ -2602,9 +2614,14 @@ Inbound.VLESSSettings = class extends Inbound.Settings { json.selectedAuth = this.selectedAuth; } - // Only include testseed if at least one client has a flow set - const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== ''); - if (hasFlow && this.testseed && this.testseed.length >= 4) { + // testseed is only meaningful for the exact xtls-rprx-vision flow, and only when + // the user supplied a complete 4-positive-int array. Otherwise omit and let the + // backend fall back to its safe defaults. + const hasVisionFlow = this.vlesses && this.vlesses.some(v => v.flow === TLS_FLOW_CONTROL.VISION); + if (hasVisionFlow + && Array.isArray(this.testseed) + && this.testseed.length === 4 + && this.testseed.every(v => Number.isInteger(v) && v > 0)) { json.testseed = this.testseed; } diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index f312ed96..bc4725c2 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -1139,11 +1139,11 @@ class Outbound extends CommonClass { return false; } - // Vision seed applies only when vision flow is selected + // Vision seed applies only when the XTLS Vision (TCP/TLS) flow is selected. + // Excludes the UDP variant per spec. canEnableVisionSeed() { if (!this.canEnableTlsFlow()) return false; - const flow = this.settings?.flow; - return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443; + return this.settings?.flow === TLS_FLOW_CONTROL.VISION; } canEnableReality() { @@ -1799,7 +1799,7 @@ Outbound.VmessSettings = class extends CommonClass { } }; Outbound.VLESSSettings = class extends CommonClass { - constructor(address, port, id, flow, encryption, reverseTag = '', reverseSniffing = new ReverseSniffing(), testpre = 0, testseed = [900, 500, 900, 256]) { + constructor(address, port, id, flow, encryption, reverseTag = '', reverseSniffing = new ReverseSniffing(), testpre = 0, testseed = []) { super(); this.address = address; this.port = port; @@ -1814,6 +1814,12 @@ Outbound.VLESSSettings = class extends CommonClass { static fromJson(json = {}) { if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings(); + const saved = json.testseed; + const testseed = (Array.isArray(saved) + && saved.length === 4 + && saved.every(v => Number.isInteger(v) && v > 0)) + ? saved + : []; return new Outbound.VLESSSettings( json.address, json.port, @@ -1823,7 +1829,7 @@ Outbound.VLESSSettings = class extends CommonClass { json.reverse?.tag || '', ReverseSniffing.fromJson(json.reverse?.sniffing || {}), json.testpre || 0, - json.testseed && json.testseed.length >= 4 ? json.testseed : [900, 500, 900, 256] + testseed, ); } @@ -1843,12 +1849,14 @@ Outbound.VLESSSettings = class extends CommonClass { sniffing: JSON.stringify(reverseSniffing) === JSON.stringify(defaultReverseSniffing) ? {} : reverseSniffing, }; } - // Only include Vision settings when flow is set - if (this.flow && this.flow !== '') { + // Vision-specific knobs are only meaningful for the exact xtls-rprx-vision flow. + if (this.flow === TLS_FLOW_CONTROL.VISION) { if (this.testpre > 0) { result.testpre = this.testpre; } - if (this.testseed && this.testseed.length >= 4) { + if (Array.isArray(this.testseed) + && this.testseed.length === 4 + && this.testseed.every(v => Number.isInteger(v) && v > 0)) { result.testseed = this.testseed; } } diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html index f8ee1542..fd566e9d 100644 --- a/web/html/form/protocol/vless.html +++ b/web/html/form/protocol/vless.html @@ -81,30 +81,38 @@