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:
MHSanaei
2026-05-28 21:17:49 +02:00
parent 1fd2c1333c
commit 798e18b6ee
22 changed files with 88 additions and 58 deletions
+4 -4
View File
@@ -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>;