mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-23 19:37:13 +00:00
feat(frontend): multi-node cloning initial implementation (#6216)
* feat(frontend): multinode cloning initial implementation * fix(frontend): harden live node detection in multinode cloning Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(frontend): add aria label to clone inbound modal * fix(frontend): shallow copy inbound settings during cloning * fix(frontend): avoid potential port conflict during testing * fix(frontend): selection buttons and websocket selection reset fix --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { RandomUtil } from '@/utils';
|
||||
import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults';
|
||||
import { coerceInboundJsonField, type DBInbound } from '@/models/dbinbound';
|
||||
|
||||
/*
|
||||
* Payload for POST /panel/api/inbounds/add reproducing `dbInbound` as a
|
||||
* staged copy: fresh port, empty client list (emails are unique panel-wide
|
||||
* and UUIDs must not repeat across nodes), disabled, no tag (the backend
|
||||
* regenerates one with the correct per-node prefix), cleared listen (listen
|
||||
* addresses are node-local). `nodeId === null` targets the local panel; the
|
||||
* field is omitted from the wire payload then, matching the add-form adapter.
|
||||
*/
|
||||
export function buildClonePayload(dbInbound: DBInbound, port: number, nodeId: number | null) {
|
||||
let clonedSettings: string;
|
||||
try {
|
||||
const raw = { ...coerceInboundJsonField(dbInbound.settings) };
|
||||
raw.clients = [];
|
||||
clonedSettings = JSON.stringify(raw);
|
||||
} catch {
|
||||
const fallback = createDefaultInboundSettings(dbInbound.protocol);
|
||||
clonedSettings = fallback ? JSON.stringify(fallback, null, 2) : '{}';
|
||||
}
|
||||
const streamSettingsString = typeof dbInbound.streamSettings === 'string'
|
||||
? dbInbound.streamSettings
|
||||
: JSON.stringify(dbInbound.streamSettings ?? {});
|
||||
const sniffingString = typeof dbInbound.sniffing === 'string'
|
||||
? dbInbound.sniffing
|
||||
: JSON.stringify(dbInbound.sniffing ?? {});
|
||||
return {
|
||||
up: 0,
|
||||
down: 0,
|
||||
total: 0,
|
||||
remark: `${dbInbound.remark} (clone)`,
|
||||
enable: false,
|
||||
expiryTime: 0,
|
||||
listen: '',
|
||||
port,
|
||||
protocol: dbInbound.protocol,
|
||||
settings: clonedSettings,
|
||||
streamSettings: streamSettingsString,
|
||||
sniffing: sniffingString,
|
||||
shareAddrStrategy: dbInbound.shareAddrStrategy,
|
||||
shareAddr: dbInbound.shareAddr,
|
||||
...(nodeId != null ? { nodeId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Random clone port in the add-form's range, avoiding ports already bound on
|
||||
* the target node (client-side pre-check; the backend's node-scoped conflict
|
||||
* check stays the final arbiter). A few random tries cover the common sparse
|
||||
* case; a target so dense that those all miss falls back to a deterministic
|
||||
* scan so a free port is always found when one exists.
|
||||
*/
|
||||
export function pickClonePort(used: Set<number> | undefined): number {
|
||||
let port = RandomUtil.randomInteger(10000, 60000);
|
||||
if (!used) return port;
|
||||
for (let attempts = 0; attempts < 20 && used.has(port); attempts++) {
|
||||
port = RandomUtil.randomInteger(10000, 60000);
|
||||
}
|
||||
if (used.has(port)) {
|
||||
for (port = 10000; port <= 60000 && used.has(port); port++) { /* dense-range scan */ }
|
||||
if (port > 60000) port = RandomUtil.randomInteger(10000, 60000);
|
||||
}
|
||||
return port;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Protocols } from '@/schemas/primitives';
|
||||
|
||||
/*
|
||||
* Protocols whose inbounds can live on a sub-node (the "Deploy To" set).
|
||||
* Everything else (http, mixed, tunnel, tun, mtproto) is panel-local only.
|
||||
* Shared by the inbound form's Deploy To selector and the clone dialog's
|
||||
* target picker so the two surfaces can never drift apart.
|
||||
*/
|
||||
export const NODE_ELIGIBLE_PROTOCOLS: Readonly<Record<string, true>> = {
|
||||
[Protocols.VLESS]: true,
|
||||
[Protocols.VMESS]: true,
|
||||
[Protocols.TROJAN]: true,
|
||||
[Protocols.SHADOWSOCKS]: true,
|
||||
[Protocols.HYSTERIA]: true,
|
||||
[Protocols.WIREGUARD]: true,
|
||||
};
|
||||
Reference in New Issue
Block a user