mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-24 05:26:08 +00:00
feat(fallbacks): add per-rule dest override
Operators can now type an explicit dest (e.g. "8443", "127.0.0.1:8443", "/dev/shm/x.sock") on each fallback row to override the auto-resolved child listen+port. Empty keeps the existing auto behavior. Adds the column to inbound_fallbacks (GORM AutoMigrate), threads it through the panel form, API docs, and translations.
This commit is contained in:
@@ -199,12 +199,12 @@ export const sections: readonly Section[] = [
|
||||
{
|
||||
method: 'GET',
|
||||
path: '/panel/api/inbounds/:id/fallbacks',
|
||||
summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/xver match criteria.',
|
||||
summary: 'List the fallback rules attached to a master VLESS/Trojan TCP-TLS inbound. Each rule links one child inbound (the dest) to optional SNI/ALPN/path/dest/xver match criteria. When dest is empty the child inbound\'s listen+port is used.',
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
|
||||
],
|
||||
response:
|
||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "masterId": 10,\n "childId": 11,\n "name": "",\n "alpn": "",\n "path": "/vlws",\n "xver": 2,\n "sortOrder": 0\n }\n ]\n}',
|
||||
'{\n "success": true,\n "obj": [\n {\n "id": 1,\n "masterId": 10,\n "childId": 11,\n "name": "",\n "alpn": "",\n "path": "/vlws",\n "dest": "",\n "xver": 2,\n "sortOrder": 0\n }\n ]\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
@@ -212,9 +212,9 @@ export const sections: readonly Section[] = [
|
||||
summary: 'Replace the entire fallback list for a master inbound. Body is JSON. Triggers an Xray restart.',
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Master inbound ID.' },
|
||||
{ name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, xver, sortOrder} entries.' },
|
||||
{ name: 'fallbacks', in: 'body (json)', type: 'object[]', desc: 'Array of {childId, name, alpn, path, dest, xver, sortOrder} entries. Leave dest empty to auto-resolve from the child inbound\'s listen+port; set it (e.g. "8443", "127.0.0.1:8443", "/dev/shm/x.sock") to override.' },
|
||||
],
|
||||
body: '{\n "fallbacks": [\n { "childId": 11, "path": "/vlws", "xver": 2 },\n { "childId": 12, "alpn": "h2" }\n ]\n}',
|
||||
body: '{\n "fallbacks": [\n { "childId": 11, "path": "/vlws", "xver": 2 },\n { "childId": 12, "alpn": "h2", "dest": "8443" }\n ]\n}',
|
||||
response: '{\n "success": true,\n "msg": "Inbound updated"\n}',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -365,13 +365,21 @@ export default function InboundFormModal({
|
||||
return;
|
||||
}
|
||||
setFallbacks(
|
||||
(msg.obj as { childId: number; name?: string; alpn?: string; path?: string; xver?: number }[])
|
||||
(msg.obj as {
|
||||
childId: number;
|
||||
name?: string;
|
||||
alpn?: string;
|
||||
path?: string;
|
||||
dest?: string;
|
||||
xver?: number;
|
||||
}[])
|
||||
.map((r) => ({
|
||||
rowKey: `fb-${++fallbackKeyRef.current}`,
|
||||
childId: r.childId,
|
||||
name: r.name || '',
|
||||
alpn: r.alpn || '',
|
||||
path: r.path || '',
|
||||
dest: r.dest || '',
|
||||
xver: r.xver || 0,
|
||||
})),
|
||||
);
|
||||
@@ -385,6 +393,7 @@ export default function InboundFormModal({
|
||||
name: c.name,
|
||||
alpn: c.alpn,
|
||||
path: c.path,
|
||||
dest: c.dest,
|
||||
xver: Number(c.xver) || 0,
|
||||
sortOrder: i,
|
||||
})),
|
||||
@@ -437,6 +446,7 @@ export default function InboundFormModal({
|
||||
name: '',
|
||||
alpn: '',
|
||||
path: '',
|
||||
dest: '',
|
||||
xver: 0,
|
||||
}]);
|
||||
};
|
||||
@@ -445,11 +455,11 @@ export default function InboundFormModal({
|
||||
setFallbacks((prev) => prev.map((r) => {
|
||||
if (r.rowKey !== rowKey) return r;
|
||||
// When the picker selects a new child inbound and the row hasn't
|
||||
// been hand-edited yet (sni/alpn/path all blank, xver = 0), pull
|
||||
// the SNI/ALPN/Path defaults off that child. Operators who
|
||||
// been hand-edited yet (sni/alpn/path/dest all blank, xver = 0),
|
||||
// pull the SNI/ALPN/Path defaults off that child. Operators who
|
||||
// intentionally typed values keep them — we only fill the empties.
|
||||
if (typeof patch.childId === 'number' && patch.childId !== r.childId) {
|
||||
const isPristine = !r.name && !r.alpn && !r.path && r.xver === 0;
|
||||
const isPristine = !r.name && !r.alpn && !r.path && !r.dest && r.xver === 0;
|
||||
if (isPristine) return { ...r, ...patch, ...deriveFallbackDefaults(patch.childId) };
|
||||
}
|
||||
return { ...r, ...patch };
|
||||
@@ -490,6 +500,7 @@ export default function InboundFormModal({
|
||||
name: derived.name ?? '',
|
||||
alpn: derived.alpn ?? '',
|
||||
path: derived.path ?? '',
|
||||
dest: '',
|
||||
xver: derived.xver ?? 0,
|
||||
};
|
||||
});
|
||||
@@ -1079,6 +1090,12 @@ export default function InboundFormModal({
|
||||
value={record.path}
|
||||
onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
|
||||
/>
|
||||
<InputAddon>Dest</InputAddon>
|
||||
<Input
|
||||
placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
|
||||
value={record.dest}
|
||||
onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
|
||||
/>
|
||||
<InputAddon>xver</InputAddon>
|
||||
<InputNumber
|
||||
min={0}
|
||||
|
||||
@@ -78,6 +78,7 @@ export const FallbackRowSchema = z.object({
|
||||
name: z.string().default(''),
|
||||
alpn: z.string().default(''),
|
||||
path: z.string().default(''),
|
||||
dest: z.string().default(''),
|
||||
xver: z.number().int().min(0).max(2).default(0),
|
||||
});
|
||||
export type FallbackRow = z.infer<typeof FallbackRowSchema>;
|
||||
|
||||
Reference in New Issue
Block a user