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 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-07 14:44:33 +02:00
parent 3349dcbc13
commit 79a7e7a5b5
5 changed files with 173 additions and 102 deletions
+29 -12
View File
@@ -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;
}
+16 -8
View File
@@ -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;
}
}