Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/web/html
diff options
context:
space:
mode:
authorlolka1333 <xtrafcyz@gmail.com>2026-01-03 07:26:00 +0300
committerGitHub <noreply@github.com>2026-01-03 07:26:00 +0300
commit313a2acbf66125feb4b145a5636351ed03e666da (patch)
tree6be6fac0ced2d0dce60ba55e2feaa83c257ed720 /web/html
parentb7477302112b43a2ae037b63994c59e85f9c0687 (diff)
feat: Add WebSocket support for real-time updates and enhance VLESS settings (#3605)
* feat: add support for trusted X-Forwarded-For and testseed parameters in VLESS settings * chore: update Xray Core version to 25.12.8 in release workflow * chore: update Xray Core version to 25.12.8 in Docker initialization script * chore: bump version to 2.8.6 and add watcher for security changes in inbound modal * refactor: remove default and random seed buttons from outbound form * refactor: update VLESS form to rename 'Test Seed' to 'Vision Seed' and change button functionality for seed generation * refactor: enhance TLS settings form layout with improved button styling and spacing * feat: integrate WebSocket support for real-time updates on inbounds and Xray service status * chore: downgrade version to 2.8.5 * refactor: translate comments to English * fix: ensure testseed is initialized correctly for VLESS protocol and improve client handling in inbound modal * refactor: simplify VLESS divider condition by removing unnecessary flow checks * fix: add fallback date formatting for cases when IntlUtil is not available * refactor: simplify WebSocket message handling by removing batching and ensuring individual message delivery * refactor: disable WebSocket notifications in inbound and index HTML files * refactor: enhance VLESS testseed initialization and button functionality in inbound modal * fix: * refactor: ensure proper WebSocket URL construction by normalizing basePath * fix: * fix: * fix: * refactor: update testseed methods for improved reactivity and binding in VLESS form * logger info to debug --------- Co-authored-by: lolka1333 <test123@gmail.com>
Diffstat (limited to 'web/html')
-rw-r--r--web/html/common/page.html1
-rw-r--r--web/html/form/outbound.html31
-rw-r--r--web/html/form/protocol/vless.html30
-rw-r--r--web/html/form/stream/stream_sockopt.html9
-rw-r--r--web/html/form/tls_settings.html18
-rw-r--r--web/html/inbounds.html121
-rw-r--r--web/html/index.html72
-rw-r--r--web/html/modals/inbound_modal.html94
-rw-r--r--web/html/settings/xray/dns.html7
-rw-r--r--web/html/xray.html19
10 files changed, 369 insertions, 33 deletions
diff --git a/web/html/common/page.html b/web/html/common/page.html
index c0a7ca63..0af63afb 100644
--- a/web/html/common/page.html
+++ b/web/html/common/page.html
@@ -49,6 +49,7 @@
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
+<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
{{ end }}
{{ define "page/body_end" }}
diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html
index aa6aa323..1926c30e 100644
--- a/web/html/form/outbound.html
+++ b/web/html/form/outbound.html
@@ -239,6 +239,28 @@
</a-select>
</a-form-item>
</template>
+ <!-- XTLS Vision Advanced Settings -->
+ <template v-if="outbound.protocol === Protocols.VLESS && (outbound.settings.flow === 'xtls-rprx-vision' || outbound.settings.flow === 'xtls-rprx-vision-udp443')">
+ <a-form-item label="Vision Pre-Connect">
+ <a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }" placeholder="0"></a-input-number>
+ </a-form-item>
+ <a-form-item label="Vision Seed">
+ <a-row :gutter="8">
+ <a-col :span="6">
+ <a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
+ </a-col>
+ </a-row>
+ </a-form-item>
+ </template>
</template>
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
@@ -501,6 +523,15 @@
<a-form-item label="Penetrate">
<a-switch v-model="outbound.stream.sockopt.penetrate"></a-switch>
</a-form-item>
+ <a-form-item label="Trusted X-Forwarded-For">
+ <a-select mode="tags" v-model="outbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
+ <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
+ <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
+ <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
+ </a-select>
+ </a-form-item>
</template>
<!-- mux settings -->
diff --git a/web/html/form/protocol/vless.html b/web/html/form/protocol/vless.html
index 140b9c1a..ad5b4265 100644
--- a/web/html/form/protocol/vless.html
+++ b/web/html/form/protocol/vless.html
@@ -39,6 +39,7 @@
</a-space>
</a-form-item>
</a-form>
+ <a-divider v-if="inbound.settings.selectedAuth" :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
@@ -69,4 +70,33 @@
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
+<template v-if="inbound.settings.vlesses.some(c => c.flow === 'xtls-rprx-vision' || c.flow === 'xtls-rprx-vision-udp443')">
+ <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+ <a-form-item label="Vision Seed">
+ <a-row :gutter="8">
+ <a-col :span="6">
+ <a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900" @change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500" @change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900" @change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
+ </a-col>
+ <a-col :span="6">
+ <a-input-number :value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256" @change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
+ </a-col>
+ </a-row>
+ <a-space :size="8" :style="{ marginTop: '8px' }">
+ <a-button type="primary" @click="setRandomTestseed">
+ Rand
+ </a-button>
+ <a-button @click="resetTestseed">
+ Reset
+ </a-button>
+ </a-space>
+ </a-form-item>
+ </a-form>
+ <a-divider :style="{ margin: '5px 0' }"></a-divider>
+</template>
{{end}}
diff --git a/web/html/form/stream/stream_sockopt.html b/web/html/form/stream/stream_sockopt.html
index 4480594a..062b83df 100644
--- a/web/html/form/stream/stream_sockopt.html
+++ b/web/html/form/stream/stream_sockopt.html
@@ -61,6 +61,15 @@
<a-form-item label="Interface Name">
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
</a-form-item>
+ <a-form-item label="Trusted X-Forwarded-For">
+ <a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
+ <a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
+ <a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
+ <a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
+ </a-select>
+ </a-form-item>
</template>
</a-form>
{{end}}
diff --git a/web/html/form/tls_settings.html b/web/html/form/tls_settings.html
index c3844a7f..3723130e 100644
--- a/web/html/form/tls_settings.html
+++ b/web/html/form/tls_settings.html
@@ -60,16 +60,20 @@
<a-form-item label="VerifyPeerCertInNames">
<a-input v-model.trim="inbound.stream.tls.verifyPeerCertInNames"></a-input>
</a-form-item>
+ <a-divider :style="{ margin: '3px 0' }"></a-divider>
<template v-for="cert,index in inbound.stream.tls.certs">
<a-form-item label='{{ i18n "certificate" }}'>
- <a-radio-group v-model="cert.useFile" button-style="solid">
- <a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
- <a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
+ <a-radio-group v-model="cert.useFile" button-style="solid" :style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
+ <a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
+ <a-radio-button :value="false" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
</a-radio-group>
- <a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"
- :style="{ marginLeft: '10px' }"></a-button>
- <a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
- @click="inbound.stream.tls.removeCert(index)" :style="{ marginLeft: '10px' }"></a-button>
+ </a-form-item>
+ <a-form-item label=" ">
+ <a-space>
+ <a-button icon="plus" v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()"></a-button>
+ <a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
+ @click="inbound.stream.tls.removeCert(index)"></a-button>
+ </a-space>
</a-form-item>
<template v-if="cert.useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
diff --git a/web/html/inbounds.html b/web/html/inbounds.html
index 86bde2c8..4e1149ae 100644
--- a/web/html/inbounds.html
+++ b/web/html/inbounds.html
@@ -1128,8 +1128,11 @@
},
openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
clients = this.getInboundClients(dbInbound);
+ if (!clients || !Array.isArray(clients)) return;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0) return;
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
@@ -1144,11 +1147,14 @@
});
},
findIndexOfClient(protocol, clients, client) {
+ if (!clients || !Array.isArray(clients) || !client) {
+ return -1;
+ }
switch (protocol) {
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
- return clients.findIndex(item => item.password === client.password && item.email === client.email);
- default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
+ return clients.findIndex(item => item && item.password === client.password && item.email === client.email);
+ default: return clients.findIndex(item => item && item.id === client.id && item.email === client.email);
}
},
async addClient(clients, dbInboundId, modal) {
@@ -1271,11 +1277,15 @@
},
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
index = 0;
if (dbInbound.isMultiUser()) {
inbound = dbInbound.toInbound();
- clients = inbound.clients;
- index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ clients = inbound && inbound.clients ? inbound.clients : null;
+ if (clients && Array.isArray(clients)) {
+ index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0) index = 0;
+ }
}
newDbInbound = this.checkFallback(dbInbound);
infoModal.show(newDbInbound, index);
@@ -1288,9 +1298,12 @@
async switchEnableClient(dbInboundId, client) {
this.loading()
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ if (!dbInbound) return;
inbound = dbInbound.toInbound();
- clients = inbound.clients;
+ clients = inbound && inbound.clients ? inbound.clients : null;
+ if (!clients || !Array.isArray(clients)) return;
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ if (index < 0 || !clients[index]) return;
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
@@ -1303,7 +1316,9 @@
}
},
getInboundClients(dbInbound) {
- return dbInbound.toInbound().clients;
+ if (!dbInbound) return null;
+ const inbound = dbInbound.toInbound();
+ return inbound && inbound.clients ? inbound.clients : null;
},
resetClientTraffic(client, dbInboundId, confirmation = true) {
if (confirmation) {
@@ -1443,7 +1458,12 @@
formatLastOnline(email) {
const ts = this.getLastOnline(email)
if (!ts) return '-'
- return IntlUtil.formatDate(ts)
+ // Check if IntlUtil is available (may not be loaded yet)
+ if (typeof IntlUtil !== 'undefined' && IntlUtil.formatDate) {
+ return IntlUtil.formatDate(ts)
+ }
+ // Fallback to simple date formatting if IntlUtil is not available
+ return new Date(ts).toLocaleString()
},
isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
@@ -1567,13 +1587,88 @@
}
this.loading();
this.getDefaultSettings();
- if (this.isRefreshEnabled) {
- this.startDataRefreshLoop();
- }
- else {
- this.getDBInbounds();
+
+ // Initial data fetch
+ this.getDBInbounds().then(() => {
+ this.loading(false);
+ });
+
+ // Setup WebSocket for real-time updates
+ if (window.wsClient) {
+ window.wsClient.connect();
+
+ // Listen for inbounds updates
+ window.wsClient.on('inbounds', (payload) => {
+ if (payload && Array.isArray(payload)) {
+ // Use setInbounds to properly convert to DBInbound objects with methods
+ this.setInbounds(payload);
+ this.searchInbounds(this.searchKey);
+ }
+ });
+
+ // Listen for traffic updates
+ window.wsClient.on('traffic', (payload) => {
+ if (payload && payload.clientTraffics && Array.isArray(payload.clientTraffics)) {
+ // Update client traffic statistics
+ payload.clientTraffics.forEach(clientTraffic => {
+ const dbInbound = this.dbInbounds.find(ib => {
+ if (!ib) return false;
+ const clients = this.getInboundClients(ib);
+ return clients && Array.isArray(clients) && clients.some(c => c && c.email === clientTraffic.email);
+ });
+ if (dbInbound && dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
+ const stats = dbInbound.clientStats.find(s => s && s.email === clientTraffic.email);
+ if (stats) {
+ stats.up = clientTraffic.up || stats.up;
+ stats.down = clientTraffic.down || stats.down;
+ stats.total = clientTraffic.total || stats.total;
+ }
+ }
+ });
+ }
+
+ // Update online clients list in real-time
+ if (payload && Array.isArray(payload.onlineClients)) {
+ this.onlineClients = payload.onlineClients;
+ // Recalculate client counts to update online status
+ this.dbInbounds.forEach(dbInbound => {
+ const inbound = this.inbounds.find(ib => ib.id === dbInbound.id);
+ if (inbound && this.clientCount[dbInbound.id]) {
+ this.clientCount[dbInbound.id] = this.getClientCounts(dbInbound, inbound);
+ }
+ });
+ }
+
+ // Update last online map in real-time
+ if (payload && payload.lastOnlineMap && typeof payload.lastOnlineMap === 'object') {
+ this.lastOnlineMap = { ...this.lastOnlineMap, ...payload.lastOnlineMap };
+ }
+ });
+
+ // Notifications disabled - white notifications are not needed
+
+ // Fallback to polling if WebSocket fails
+ window.wsClient.on('error', () => {
+ console.warn('WebSocket connection failed, falling back to polling');
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
+ });
+
+ window.wsClient.on('disconnected', () => {
+ if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
+ console.warn('WebSocket reconnection failed, falling back to polling');
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
+ }
+ });
+ } else {
+ // Fallback to polling if WebSocket is not available
+ if (this.isRefreshEnabled) {
+ this.startDataRefreshLoop();
+ }
}
- this.loading(false);
},
computed: {
total() {
diff --git a/web/html/index.html b/web/html/index.html
index 9cbb019d..bbbbb708 100644
--- a/web/html/index.html
+++ b/web/html/index.html
@@ -1102,6 +1102,20 @@
});
fileInput.click();
},
+ startPolling() {
+ // Fallback polling mechanism
+ const pollInterval = setInterval(async () => {
+ if (window.wsClient && window.wsClient.isConnected) {
+ clearInterval(pollInterval);
+ return;
+ }
+ try {
+ await this.getStatus();
+ } catch (e) {
+ console.error(e);
+ }
+ }, 2000);
+ },
},
async mounted() {
if (window.location.protocol !== "https:") {
@@ -1113,13 +1127,57 @@
this.ipLimitEnable = msg.obj.ipLimitEnable;
}
- while (true) {
- try {
- await this.getStatus();
- } catch (e) {
- console.error(e);
- }
- await PromiseUtil.sleep(2000);
+ // Initial status fetch
+ await this.getStatus();
+
+ // Setup WebSocket for real-time updates
+ if (window.wsClient) {
+ window.wsClient.connect();
+
+ // Listen for status updates
+ window.wsClient.on('status', (payload) => {
+ this.setStatus(payload);
+ });
+
+ // Listen for Xray state changes
+ window.wsClient.on('xray_state', (payload) => {
+ if (this.status && this.status.xray) {
+ this.status.xray.state = payload.state;
+ this.status.xray.errorMsg = payload.errorMsg || '';
+ switch (payload.state) {
+ case 'running':
+ this.status.xray.color = "green";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusRunning" }}';
+ break;
+ case 'stop':
+ this.status.xray.color = "orange";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusStop" }}';
+ break;
+ case 'error':
+ this.status.xray.color = "red";
+ this.status.xray.stateMsg = '{{ i18n "pages.index.xrayStatusError" }}';
+ break;
+ }
+ }
+ });
+
+ // Notifications disabled - white notifications are not needed
+
+ // Fallback to polling if WebSocket fails
+ window.wsClient.on('error', () => {
+ console.warn('WebSocket connection failed, falling back to polling');
+ this.startPolling();
+ });
+
+ window.wsClient.on('disconnected', () => {
+ if (window.wsClient.reconnectAttempts >= window.wsClient.maxReconnectAttempts) {
+ console.warn('WebSocket reconnection failed, falling back to polling');
+ this.startPolling();
+ }
+ });
+ } else {
+ // Fallback to polling if WebSocket is not available
+ this.startPolling();
}
},
});
diff --git a/web/html/modals/inbound_modal.html b/web/html/modals/inbound_modal.html
index 3c844381..c3883285 100644
--- a/web/html/modals/inbound_modal.html
+++ b/web/html/modals/inbound_modal.html
@@ -6,7 +6,8 @@
</a-modal>
<script>
- const inModal = {
+ // Make inModal globally available to ensure it works with any base path
+ const inModal = window.inModal = {
title: '',
visible: false,
confirmLoading: false,
@@ -26,6 +27,14 @@
} else {
this.inbound = new Inbound();
}
+ // Always ensure testseed is initialized for VLESS protocol (even if vision flow is not set yet)
+ // This ensures Vue reactivity works properly
+ if (this.inbound.protocol === Protocols.VLESS && this.inbound.settings) {
+ if (!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) {
this.dbInbound = new DBInbound(dbInbound);
} else {
@@ -42,9 +51,43 @@
loading(loading = true) {
inModal.confirmLoading = loading;
},
+ // Vision Seed methods - always available regardless of Vue context
+ updateTestseed(index, value) {
+ // Use inModal.inbound explicitly to ensure correct context
+ if (!inModal.inbound || !inModal.inbound.settings) return;
+ // Ensure testseed is initialized
+ if (!inModal.inbound.settings.testseed || !Array.isArray(inModal.inbound.settings.testseed)) {
+ inModal.inbound.settings.testseed = [900, 500, 900, 256];
+ }
+ // Ensure array has enough elements
+ while (inModal.inbound.settings.testseed.length <= index) {
+ inModal.inbound.settings.testseed.push(0);
+ }
+ // Update value
+ inModal.inbound.settings.testseed[index] = value;
+ },
+ setRandomTestseed() {
+ // Use inModal.inbound explicitly to ensure correct context
+ if (!inModal.inbound || !inModal.inbound.settings) return;
+ // Ensure testseed is initialized
+ 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 = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
+ },
+ resetTestseed() {
+ // Use inModal.inbound explicitly to ensure correct context
+ if (!inModal.inbound || !inModal.inbound.settings) return;
+ // Reset testseed to default values
+ inModal.inbound.settings.testseed = [900, 500, 900, 256].slice();
+ }
};
- new Vue({
+ // Store Vue instance globally to ensure methods are always accessible
+ let inboundModalVueInstance = null;
+
+ inboundModalVueInstance = new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-modal',
data: {
@@ -60,7 +103,7 @@
return inModal.isEdit;
},
get client() {
- return inModal.inbound.clients[0];
+ return inModal.inbound && inModal.inbound.clients && inModal.inbound.clients.length > 0 ? inModal.inbound.clients[0] : null;
},
get datepicker() {
return app.datepicker;
@@ -87,6 +130,28 @@
}
}
},
+ watch: {
+ 'inModal.inbound.stream.security'(newVal, oldVal) {
+ // Clear flow when security changes from reality/tls to none
+ if (inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
+ inModal.inbound.settings.vlesses.forEach(client => {
+ client.flow = "";
+ });
+ }
+ },
+ // Ensure testseed is always initialized when vision flow is enabled
+ 'inModal.inbound.settings.vlesses': {
+ handler() {
+ if (inModal.inbound.protocol === Protocols.VLESS && inModal.inbound.settings && inModal.inbound.settings.vlesses) {
+ const hasVisionFlow = inModal.inbound.settings.vlesses.some(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
+ }
+ },
methods: {
streamNetworkChange() {
if (!inModal.inbound.canEnableTls()) {
@@ -204,8 +269,29 @@
this.inbound.settings.decryption = 'none';
this.inbound.settings.encryption = 'none';
this.inbound.settings.selectedAuth = undefined;
+ },
+ // Vision Seed methods - must be in Vue methods for proper binding
+ updateTestseed(index, value) {
+ // Ensure testseed is initialized
+ if (!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
+ while (this.inbound.settings.testseed.length <= index) {
+ this.inbound.settings.testseed.push(0);
+ }
+ // Update value using Vue.set for reactivity
+ this.$set(this.inbound.settings.testseed, index, value);
+ },
+ setRandomTestseed() {
+ // Create new array with random values and use Vue.set for reactivity
+ const newSeed = [Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000), Math.floor(Math.random()*1000)];
+ this.$set(this.inbound.settings, 'testseed', newSeed);
+ },
+ resetTestseed() {
+ // Reset testseed to default values using Vue.set for reactivity
+ this.$set(this.inbound.settings, 'testseed', [900, 500, 900, 256]);
}
-
},
});
diff --git a/web/html/settings/xray/dns.html b/web/html/settings/xray/dns.html
index ba768cb8..8a18bbb4 100644
--- a/web/html/settings/xray/dns.html
+++ b/web/html/settings/xray/dns.html
@@ -56,6 +56,13 @@
<a-switch v-model="dnsDisableFallbackIfMatch"></a-switch>
</template>
</a-setting-list-item>
+ <a-setting-list-item paddings="small">
+ <template #title>{{ i18n "pages.xray.dns.enableParallelQuery" }}</template>
+ <template #description>{{ i18n "pages.xray.dns.enableParallelQueryDesc" }}</template>
+ <template #control>
+ <a-switch v-model="dnsEnableParallelQuery"></a-switch>
+ </template>
+ </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.dns.useSystemHosts" }}</template>
diff --git a/web/html/xray.html b/web/html/xray.html
index f4d89c84..ce5e1ab7 100644
--- a/web/html/xray.html
+++ b/web/html/xray.html
@@ -269,7 +269,7 @@
tag: "direct",
protocol: "freedom"
},
- routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
+ routingDomainStrategies: ["AsIs", "IpIfNonMatch", "IpOnDemand"],
log: {
loglevel: ["none", "debug", "info", "warning", "error"],
access: ["none", "./access.log"],
@@ -1315,7 +1315,8 @@
newTemplateSettings.dns = {
servers: [],
queryStrategy: "UseIP",
- tag: "dns_inbound"
+ tag: "dns_inbound",
+ enableParallelQuery: false
};
newTemplateSettings.fakedns = null;
} else {
@@ -1391,6 +1392,20 @@
this.templateSettings = newTemplateSettings;
}
},
+ dnsEnableParallelQuery: {
+ get: function () {
+ return this.enableDNS ? (this.templateSettings.dns.enableParallelQuery || false) : false;
+ },
+ set: function (newValue) {
+ newTemplateSettings = this.templateSettings;
+ if (newValue) {
+ newTemplateSettings.dns.enableParallelQuery = newValue;
+ } else {
+ delete newTemplateSettings.dns.enableParallelQuery
+ }
+ this.templateSettings = newTemplateSettings;
+ }
+ },
dnsUseSystemHosts: {
get: function () {
return this.enableDNS ? this.templateSettings.dns.useSystemHosts : false;