feat(node): per node outbound routing (#5275)

* feat: add per-node outbound routing for panel-to-node connections

* feat(ui): add outbound tag selector to node form with i18n

* fix(xray): avoid potential overflow warning in node egress rule allocation

* chore: run "npm run gen"

* fix

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Nikan Zeyaei
2026-06-15 00:40:52 +03:30
committed by GitHub
parent 2188830612
commit 05ad7f417c
33 changed files with 443 additions and 41 deletions
@@ -18,6 +18,7 @@ import type { RemoteInboundOption } from '@/api/queries/useNodeMutations';
import type { Msg } from '@/utils';
import { NodeFormSchema, type NodeFormValues, type ProbeResult } from '@/schemas/node';
import { antdRule } from '@/utils/zodForm';
import { useOutboundTags } from '@/api/queries/useOutboundTags';
import './NodeFormModal.css';
type Mode = 'add' | 'edit';
@@ -49,6 +50,7 @@ function defaultValues(): NodeFormValues {
pinnedCertSha256: '',
inboundSyncMode: 'all',
inboundTags: [],
outboundTag: '',
};
}
@@ -75,6 +77,7 @@ export default function NodeFormModal({
const scheme = Form.useWatch('scheme', form) ?? 'https';
const tlsVerifyMode = Form.useWatch('tlsVerifyMode', form) ?? 'verify';
const inboundSyncMode = Form.useWatch('inboundSyncMode', form) ?? 'all';
const { data: outboundTags } = useOutboundTags({ excludeBlackhole: true });
useEffect(() => {
if (!open) return;
@@ -117,6 +120,7 @@ export default function NodeFormModal({
pinnedCertSha256: values.tlsVerifyMode === 'pin' ? values.pinnedCertSha256.trim() : '',
inboundSyncMode: values.inboundSyncMode,
inboundTags: values.inboundSyncMode === 'selected' ? values.inboundTags : [],
outboundTag: values.outboundTag || '',
};
}
@@ -356,6 +360,19 @@ export default function NodeFormModal({
<Input.Password placeholder={t('pages.nodes.apiTokenPlaceholder')} />
</Form.Item>
<Form.Item
label={t('pages.nodes.outboundTag')}
name="outboundTag"
extra={t('pages.nodes.outboundTagHint')}
>
<Select
allowClear
showSearch
placeholder={t('pages.nodes.outboundTagPlaceholder')}
options={(outboundTags ?? []).map((tag) => ({ value: tag, label: tag }))}
/>
</Form.Item>
<Form.Item
label={t('pages.nodes.inboundSyncMode')}
name="inboundSyncMode"