mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-08 14:36:13 +00:00
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:
@@ -1763,12 +1763,13 @@ class Inbound extends XrayCommonClass {
|
|||||||
return false;
|
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() {
|
canEnableVisionSeed() {
|
||||||
if (!this.canEnableTlsFlow()) return false;
|
if (!this.canEnableTlsFlow()) return false;
|
||||||
const clients = this.settings?.vlesses;
|
const clients = this.settings?.vlesses;
|
||||||
if (!Array.isArray(clients)) return false;
|
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() {
|
canEnableReality() {
|
||||||
@@ -2543,7 +2544,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
encryption = "none",
|
encryption = "none",
|
||||||
fallbacks = [],
|
fallbacks = [],
|
||||||
selectedAuth = undefined,
|
selectedAuth = undefined,
|
||||||
testseed = [900, 500, 900, 256],
|
testseed = [],
|
||||||
) {
|
) {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.vlesses = vlesses;
|
this.vlesses = vlesses;
|
||||||
@@ -2562,12 +2563,23 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
this.fallbacks.splice(index, 1);
|
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 = {}) {
|
static fromJson(json = {}) {
|
||||||
// Ensure testseed is always initialized as an array
|
// Preserve a saved testseed only if it's a valid 4-positive-int array; otherwise leave empty
|
||||||
let testseed = [900, 500, 900, 256];
|
// so toJson omits it and the form falls back to placeholder defaults.
|
||||||
if (json.testseed && Array.isArray(json.testseed) && json.testseed.length >= 4) {
|
const saved = json.testseed;
|
||||||
testseed = json.testseed;
|
const testseed = (Array.isArray(saved)
|
||||||
}
|
&& saved.length === 4
|
||||||
|
&& saved.every(v => Number.isInteger(v) && v > 0))
|
||||||
|
? saved
|
||||||
|
: [];
|
||||||
|
|
||||||
const obj = new Inbound.VLESSSettings(
|
const obj = new Inbound.VLESSSettings(
|
||||||
Protocols.VLESS,
|
Protocols.VLESS,
|
||||||
@@ -2576,7 +2588,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
json.encryption,
|
json.encryption,
|
||||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||||
json.selectedAuth,
|
json.selectedAuth,
|
||||||
testseed
|
testseed,
|
||||||
);
|
);
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -2602,9 +2614,14 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
json.selectedAuth = this.selectedAuth;
|
json.selectedAuth = this.selectedAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include testseed if at least one client has a flow set
|
// testseed is only meaningful for the exact xtls-rprx-vision flow, and only when
|
||||||
const hasFlow = this.vlesses && this.vlesses.some(vless => vless.flow && vless.flow !== '');
|
// the user supplied a complete 4-positive-int array. Otherwise omit and let the
|
||||||
if (hasFlow && this.testseed && this.testseed.length >= 4) {
|
// 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;
|
json.testseed = this.testseed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1139,11 +1139,11 @@ class Outbound extends CommonClass {
|
|||||||
return false;
|
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() {
|
canEnableVisionSeed() {
|
||||||
if (!this.canEnableTlsFlow()) return false;
|
if (!this.canEnableTlsFlow()) return false;
|
||||||
const flow = this.settings?.flow;
|
return this.settings?.flow === TLS_FLOW_CONTROL.VISION;
|
||||||
return flow === TLS_FLOW_CONTROL.VISION || flow === TLS_FLOW_CONTROL.VISION_UDP443;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canEnableReality() {
|
canEnableReality() {
|
||||||
@@ -1799,7 +1799,7 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Outbound.VLESSSettings = 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();
|
super();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
@@ -1814,6 +1814,12 @@ Outbound.VLESSSettings = class extends CommonClass {
|
|||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
if (ObjectUtil.isEmpty(json.address) || ObjectUtil.isEmpty(json.port)) return new Outbound.VLESSSettings();
|
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(
|
return new Outbound.VLESSSettings(
|
||||||
json.address,
|
json.address,
|
||||||
json.port,
|
json.port,
|
||||||
@@ -1823,7 +1829,7 @@ Outbound.VLESSSettings = class extends CommonClass {
|
|||||||
json.reverse?.tag || '',
|
json.reverse?.tag || '',
|
||||||
ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
|
ReverseSniffing.fromJson(json.reverse?.sniffing || {}),
|
||||||
json.testpre || 0,
|
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,
|
sniffing: JSON.stringify(reverseSniffing) === JSON.stringify(defaultReverseSniffing) ? {} : reverseSniffing,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Only include Vision settings when flow is set
|
// Vision-specific knobs are only meaningful for the exact xtls-rprx-vision flow.
|
||||||
if (this.flow && this.flow !== '') {
|
if (this.flow === TLS_FLOW_CONTROL.VISION) {
|
||||||
if (this.testpre > 0) {
|
if (this.testpre > 0) {
|
||||||
result.testpre = this.testpre;
|
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;
|
result.testseed = this.testseed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,30 +81,38 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-if="inbound.canEnableVisionSeed()">
|
<template v-if="inbound.canEnableVisionSeed()">
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
<a-form-item label="Vision Seed">
|
<a-form-item
|
||||||
|
:validate-status="testseedError() ? 'error' : ''"
|
||||||
|
:help="testseedError() || ''">
|
||||||
|
<template slot="label">
|
||||||
|
Vision Seed
|
||||||
|
<a-tooltip title="Optional. Controls XTLS Vision padding. Provide exactly 4 positive integers, or leave empty to use defaults: [900, 500, 900, 256].">
|
||||||
|
<a-icon type="question-circle" :style="{ marginLeft: '4px' }"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
<a-row :gutter="8">
|
<a-row :gutter="8">
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
|
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : null"
|
||||||
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
|
@change="(val) => updateTestseed(0, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="900"
|
||||||
addon-before="[0]"></a-input-number>
|
addon-before="[0]"></a-input-number>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
|
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : null"
|
||||||
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500"
|
@change="(val) => updateTestseed(1, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="500"
|
||||||
addon-before="[1]"></a-input-number>
|
addon-before="[1]"></a-input-number>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
|
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : null"
|
||||||
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
|
@change="(val) => updateTestseed(2, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="900"
|
||||||
addon-before="[2]"></a-input-number>
|
addon-before="[2]"></a-input-number>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="6">
|
<a-col :span="6">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
|
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : null"
|
||||||
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256"
|
@change="(val) => updateTestseed(3, val)" :min="1" :max="9999" :style="{ width: '100%' }" placeholder="256"
|
||||||
addon-before="[3]"></a-input-number>
|
addon-before="[3]"></a-input-number>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -116,6 +124,10 @@
|
|||||||
Reset
|
Reset
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
<div :style="{ marginTop: '6px', fontSize: '12px', color: 'inherit', opacity: 0.65, lineHeight: 1.4 }">
|
||||||
|
Optional. Controls XTLS Vision padding behavior (used only for xtls-rprx-vision).
|
||||||
|
Provide exactly four positive integers to customize padding; otherwise leave empty to use safe defaults.
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
<a-divider :style="{ margin: '5px 0' }"></a-divider>
|
||||||
|
|||||||
@@ -16,6 +16,16 @@
|
|||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
ok() {
|
ok() {
|
||||||
|
// Block submit when Vision Seed is XRV-gated and partially/invalidly filled.
|
||||||
|
const seedErr = inModal.testseedError();
|
||||||
|
if (seedErr) {
|
||||||
|
if (typeof Vue !== "undefined" && Vue.prototype && Vue.prototype.$message) {
|
||||||
|
Vue.prototype.$message.error(seedErr);
|
||||||
|
} else {
|
||||||
|
alert(seedErr);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||||
},
|
},
|
||||||
show({
|
show({
|
||||||
@@ -33,16 +43,12 @@
|
|||||||
} else {
|
} else {
|
||||||
this.inbound = new Inbound();
|
this.inbound = new Inbound();
|
||||||
}
|
}
|
||||||
// Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)
|
// Ensure VLESS settings has a testseed array reference for Vue reactivity,
|
||||||
// This ensures Vue reactivity works properly
|
// but leave it empty so we don't auto-emit defaults — user must explicitly
|
||||||
|
// fill all four fields, or leave blank to fall back to backend defaults.
|
||||||
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
|
if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
|
||||||
if (
|
if (!Array.isArray(this.inbound.settings.testseed)) {
|
||||||
!this.inbound.settings.testseed ||
|
this.inbound.settings.testseed = [];
|
||||||
!Array.isArray(this.inbound.settings.testseed) ||
|
|
||||||
this.inbound.settings.testseed.length < 4
|
|
||||||
) {
|
|
||||||
// Create a new array to ensure Vue reactivity
|
|
||||||
this.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dbInbound) {
|
if (dbInbound) {
|
||||||
@@ -61,48 +67,50 @@
|
|||||||
loading(loading = true) {
|
loading(loading = true) {
|
||||||
inModal.confirmLoading = loading;
|
inModal.confirmLoading = loading;
|
||||||
},
|
},
|
||||||
// Vision Seed methods - always available regardless of Vue context
|
// Returns an error string when the current testseed state would be rejected,
|
||||||
|
// or "" when it's valid (empty == use defaults; full 4 positive ints == custom).
|
||||||
|
testseedError() {
|
||||||
|
if (!inModal.inbound || inModal.inbound.protocol !== Protocols.VLESS) return "";
|
||||||
|
if (typeof inModal.inbound.canEnableVisionSeed === "function"
|
||||||
|
&& !inModal.inbound.canEnableVisionSeed()) return "";
|
||||||
|
const seed = inModal.inbound.settings && inModal.inbound.settings.testseed;
|
||||||
|
if (!Array.isArray(seed) || seed.length === 0) return "";
|
||||||
|
const filled = seed.filter(v => v !== null && v !== undefined && v !== "");
|
||||||
|
if (filled.length === 0) return "";
|
||||||
|
if (seed.length !== 4 || filled.length !== 4 ||
|
||||||
|
!seed.every(v => Number.isInteger(v) && v > 0)) {
|
||||||
|
return "Provide exactly 4 positive integers or leave empty to use defaults.";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
// Vision Seed helpers — always available regardless of Vue context
|
||||||
updateTestseed(index, value) {
|
updateTestseed(index, value) {
|
||||||
// Use inModal.inbound explicitly to ensure correct context
|
|
||||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||||
// Ensure testseed is initialized
|
if (!Array.isArray(inModal.inbound.settings.testseed)) {
|
||||||
if (
|
inModal.inbound.settings.testseed = [];
|
||||||
!inModal.inbound.settings.testseed ||
|
|
||||||
!Array.isArray(inModal.inbound.settings.testseed)
|
|
||||||
) {
|
|
||||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
|
||||||
}
|
}
|
||||||
// Ensure array has enough elements
|
const seed = inModal.inbound.settings.testseed;
|
||||||
while (inModal.inbound.settings.testseed.length <= index) {
|
while (seed.length <= index) seed.push(null);
|
||||||
inModal.inbound.settings.testseed.push(0);
|
seed[index] = value;
|
||||||
|
// If user cleared every slot, collapse back to empty so we omit testseed entirely.
|
||||||
|
if (seed.every(v => v === null || v === undefined || v === "")) {
|
||||||
|
inModal.inbound.settings.testseed = [];
|
||||||
}
|
}
|
||||||
// Update value
|
|
||||||
inModal.inbound.settings.testseed[index] = value;
|
|
||||||
},
|
},
|
||||||
setRandomTestseed() {
|
setRandomTestseed() {
|
||||||
// Use inModal.inbound explicitly to ensure correct context
|
|
||||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||||
// Ensure testseed is initialized
|
// Positive integers only (>=1) so the array passes validation and gets emitted.
|
||||||
if (
|
|
||||||
!inModal.inbound.settings.testseed ||
|
|
||||||
!Array.isArray(inModal.inbound.settings.testseed) ||
|
|
||||||
inModal.inbound.settings.testseed.length < 4
|
|
||||||
) {
|
|
||||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
|
||||||
}
|
|
||||||
// Create new array with random values
|
|
||||||
inModal.inbound.settings.testseed = [
|
inModal.inbound.settings.testseed = [
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
resetTestseed() {
|
resetTestseed() {
|
||||||
// Use inModal.inbound explicitly to ensure correct context
|
|
||||||
if (!inModal.inbound || !inModal.inbound.settings) return;
|
if (!inModal.inbound || !inModal.inbound.settings) return;
|
||||||
// Reset testseed to default values
|
// Empty == "use server defaults [900, 500, 900, 256]"; placeholders show in the form.
|
||||||
inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
|
inModal.inbound.settings.testseed = [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,27 +178,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Ensure testseed is always initialized when vision flow is enabled
|
// Keep testseed as a valid array reference for Vue reactivity while the user
|
||||||
|
// toggles flows — but do NOT auto-fill defaults. Empty means "use server defaults"
|
||||||
|
// and is the only way the form omits testseed from the outbound JSON.
|
||||||
"inModal.inbound.settings.vlesses": {
|
"inModal.inbound.settings.vlesses": {
|
||||||
handler() {
|
handler() {
|
||||||
if (
|
if (
|
||||||
inModal.inbound.protocol === Protocols.VLESS &&
|
inModal.inbound.protocol === Protocols.VLESS &&
|
||||||
inModal.inbound.settings &&
|
inModal.inbound.settings &&
|
||||||
inModal.inbound.settings.vlesses
|
!Array.isArray(inModal.inbound.settings.testseed)
|
||||||
) {
|
) {
|
||||||
const hasVisionFlow = inModal.inbound.settings.vlesses.some(
|
inModal.inbound.settings.testseed = [];
|
||||||
(c) =>
|
|
||||||
c.flow === "xtls-rprx-vision" ||
|
|
||||||
c.flow === "xtls-rprx-vision-udp443",
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
hasVisionFlow &&
|
|
||||||
(!inModal.inbound.settings.testseed ||
|
|
||||||
!Array.isArray(inModal.inbound.settings.testseed) ||
|
|
||||||
inModal.inbound.settings.testseed.length < 4)
|
|
||||||
) {
|
|
||||||
inModal.inbound.settings.testseed = [900, 500, 900, 256];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
@@ -335,35 +333,36 @@
|
|||||||
this.inbound.settings.encryption = "none";
|
this.inbound.settings.encryption = "none";
|
||||||
this.inbound.settings.selectedAuth = undefined;
|
this.inbound.settings.selectedAuth = undefined;
|
||||||
},
|
},
|
||||||
// Vision Seed methods - must be in Vue methods for proper binding
|
// Vision Seed methods - must be in Vue methods for proper template binding.
|
||||||
|
// Mirror the inModal helpers but use Vue.set so the form re-renders.
|
||||||
updateTestseed(index, value) {
|
updateTestseed(index, value) {
|
||||||
// Ensure testseed is initialized
|
if (!Array.isArray(this.inbound.settings.testseed)) {
|
||||||
if (
|
this.$set(this.inbound.settings, "testseed", []);
|
||||||
!this.inbound.settings.testseed ||
|
|
||||||
!Array.isArray(this.inbound.settings.testseed)
|
|
||||||
) {
|
|
||||||
this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]);
|
|
||||||
}
|
}
|
||||||
// Ensure array has enough elements
|
const seed = this.inbound.settings.testseed;
|
||||||
while (this.inbound.settings.testseed.length <= index) {
|
while (seed.length <= index) seed.push(null);
|
||||||
this.inbound.settings.testseed.push(0);
|
this.$set(seed, index, value);
|
||||||
|
// Collapse to empty when every slot is cleared so testseed is omitted from JSON.
|
||||||
|
if (seed.every(v => v === null || v === undefined || v === "")) {
|
||||||
|
this.$set(this.inbound.settings, "testseed", []);
|
||||||
}
|
}
|
||||||
// Update value using Vue.set for reactivity
|
|
||||||
this.$set(this.inbound.settings.testseed, index, value);
|
|
||||||
},
|
},
|
||||||
setRandomTestseed() {
|
setRandomTestseed() {
|
||||||
// Create new array with random values and use Vue.set for reactivity
|
// Positive integers only (>=1) so the resulting array passes validation.
|
||||||
const newSeed = [
|
const newSeed = [
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
Math.floor(Math.random() * 1000),
|
Math.floor(Math.random() * 999) + 1,
|
||||||
];
|
];
|
||||||
this.$set(this.inbound.settings, "testseed", newSeed);
|
this.$set(this.inbound.settings, "testseed", newSeed);
|
||||||
},
|
},
|
||||||
resetTestseed() {
|
resetTestseed() {
|
||||||
// Reset testseed to default values using Vue.set for reactivity
|
// Empty == "use server defaults [900, 500, 900, 256]". Placeholders will show in the form.
|
||||||
this.$set(this.inbound.settings, "testseed", [900, 500, 900, 256]);
|
this.$set(this.inbound.settings, "testseed", []);
|
||||||
|
},
|
||||||
|
testseedError() {
|
||||||
|
return inModal.testseedError();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1213,6 +1213,28 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||||||
settingsClients[clientIndex] = interfaceClients[0]
|
settingsClients[clientIndex] = interfaceClients[0]
|
||||||
oldSettings["clients"] = settingsClients
|
oldSettings["clients"] = settingsClients
|
||||||
|
|
||||||
|
// testseed is only meaningful when at least one VLESS client uses the exact
|
||||||
|
// xtls-rprx-vision flow. The client-edit path only rewrites a single client,
|
||||||
|
// so re-check the flow set here and strip a stale testseed when nothing in the
|
||||||
|
// inbound still warrants it. The full-inbound update path already handles this
|
||||||
|
// on the JS side via VLESSSettings.toJson().
|
||||||
|
if oldInbound.Protocol == model.VLESS {
|
||||||
|
hasVisionFlow := false
|
||||||
|
for _, c := range settingsClients {
|
||||||
|
cm, ok := c.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if flow, _ := cm["flow"].(string); flow == "xtls-rprx-vision" {
|
||||||
|
hasVisionFlow = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasVisionFlow {
|
||||||
|
delete(oldSettings, "testseed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -2885,6 +2907,7 @@ func (s *InboundService) MigrationRequirements() {
|
|||||||
if ok {
|
if ok {
|
||||||
// Fix Client configuration problems
|
// Fix Client configuration problems
|
||||||
var newClients []any
|
var newClients []any
|
||||||
|
hasVisionFlow := false
|
||||||
for client_index := range clients {
|
for client_index := range clients {
|
||||||
c := clients[client_index].(map[string]any)
|
c := clients[client_index].(map[string]any)
|
||||||
|
|
||||||
@@ -2910,6 +2933,9 @@ func (s *InboundService) MigrationRequirements() {
|
|||||||
c["flow"] = ""
|
c["flow"] = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if flow, _ := c["flow"].(string); flow == "xtls-rprx-vision" {
|
||||||
|
hasVisionFlow = true
|
||||||
|
}
|
||||||
// Backfill created_at and updated_at
|
// Backfill created_at and updated_at
|
||||||
if _, ok := c["created_at"]; !ok {
|
if _, ok := c["created_at"]; !ok {
|
||||||
c["created_at"] = time.Now().Unix() * 1000
|
c["created_at"] = time.Now().Unix() * 1000
|
||||||
@@ -2918,6 +2944,15 @@ func (s *InboundService) MigrationRequirements() {
|
|||||||
newClients = append(newClients, any(c))
|
newClients = append(newClients, any(c))
|
||||||
}
|
}
|
||||||
settings["clients"] = newClients
|
settings["clients"] = newClients
|
||||||
|
|
||||||
|
// Drop orphaned testseed: VLESS-only field, only meaningful when at least
|
||||||
|
// one client uses the exact xtls-rprx-vision flow. Older versions saved it
|
||||||
|
// for any non-empty flow (including the UDP variant) or kept it after the
|
||||||
|
// flow was cleared from the client modal — clean those up here.
|
||||||
|
if inbounds[inbound_index].Protocol == model.VLESS && !hasVisionFlow {
|
||||||
|
delete(settings, "testseed")
|
||||||
|
}
|
||||||
|
|
||||||
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user