remove(field.name)}
/>
))}
@@ -147,15 +150,18 @@ function TcpMasksList({ base, form }: { base: (string | number)[]; form: FormIns
}
function TcpMaskItem({
- base, index, displayIndex, form, onRemove,
+ fieldName, displayIndex, form, listPath, onRemove,
}: {
- base: (string | number)[];
- index: number;
+ fieldName: number;
displayIndex: number;
form: FormInstance;
+ listPath: (string | number)[];
onRemove: () => void;
}) {
- const path = [...base, 'tcp', index];
+ // Absolute path for setFieldValue side effects (resetting settings on
+ // type change). All Form.Item `name=` use RELATIVE paths within the
+ // outer Form.List context.
+ const absolutePath = [...listPath, fieldName];
return (
@@ -164,9 +170,11 @@ function TcpMaskItem({
-
+
form.setFieldValue([...path, 'settings'], defaultTcpMaskSettings(v))}
+ onChange={(v) =>
+ form.setFieldValue([...absolutePath, 'settings'], defaultTcpMaskSettings(v))
+ }
options={[
{ value: 'fragment', label: 'Fragment' },
{ value: 'header-custom', label: 'Header Custom' },
@@ -177,16 +185,18 @@ function TcpMaskItem({
- (prev as Record)[String(path[0])] !== (curr as Record)[String(path[0])]
- }
+ shouldUpdate={(prev, curr) => {
+ const a = getDeep(prev, [...absolutePath, 'type']);
+ const b = getDeep(curr, [...absolutePath, 'type']);
+ return a !== b;
+ }}
>
{({ getFieldValue }) => {
- const type = getFieldValue([...path, 'type']) as string | undefined;
+ const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
if (type === 'fragment') {
return (
<>
-
+
-
+
-
+
-
+
>
@@ -210,21 +220,27 @@ function TcpMaskItem({
if (type === 'sudoku') {
return (
<>
-
-
-
-
-
+
+
+
+
+
-
+
>
);
}
if (type === 'header-custom') {
- return ;
+ return (
+
+ );
}
return null;
}}
@@ -233,11 +249,29 @@ function TcpMaskItem({
);
}
-function HeaderCustomGroups({ base, form }: { base: (string | number)[]; form: FormInstance }) {
+// Walks a deep object path safely. Used inside shouldUpdate which gets
+// the whole form values blob; we need to compare a deep field across
+// prev/curr without crashing on missing intermediates.
+function getDeep(obj: unknown, path: (string | number)[]): unknown {
+ let cur: unknown = obj;
+ for (const key of path) {
+ if (cur == null || typeof cur !== 'object') return undefined;
+ cur = (cur as Record)[key];
+ }
+ return cur;
+}
+
+function HeaderCustomGroups({
+ tcpFieldName, form, absoluteSettingsPath,
+}: {
+ tcpFieldName: number;
+ form: FormInstance;
+ absoluteSettingsPath: (string | number)[];
+}) {
return (
<>
{(['clients', 'servers'] as const).map((groupKey) => (
-
+
{(groups, { add: addGroup, remove: removeGroup }) => (
<>
@@ -254,7 +288,7 @@ function HeaderCustomGroups({ base, form }: { base: (string | number)[]; form: F
{groupKey === 'clients' ? 'Clients' : 'Servers'} Group {gi + 1}
removeGroup(group.name)} />
-
+
{(items, { add: addItem, remove: removeItem }) => (
<>
@@ -267,8 +301,9 @@ function HeaderCustomGroups({ base, form }: { base: (string | number)[]; form: F
{items.map((item) => (
removeItem(item.name)}
/>
@@ -287,8 +322,8 @@ function HeaderCustomGroups({ base, form }: { base: (string | number)[]; form: F
}
function UdpMasksList({
- base, form, isHysteria,
-}: { base: (string | number)[]; form: FormInstance; isHysteria: boolean }) {
+ base, form, isHysteria, network,
+}: { base: (string | number)[]; form: FormInstance; isHysteria: boolean; network: string }) {
return (
{(fields, { add, remove }) => (
@@ -307,11 +342,12 @@ function UdpMasksList({
{fields.map((field, mIdx) => (
remove(field.name)}
/>
))}
@@ -322,24 +358,23 @@ function UdpMasksList({
}
function UdpMaskItem({
- base, index, displayIndex, form, isHysteria, onRemove,
+ fieldName, displayIndex, form, listPath, isHysteria, network, onRemove,
}: {
- base: (string | number)[];
- index: number;
+ fieldName: number;
displayIndex: number;
form: FormInstance;
+ listPath: (string | number)[];
isHysteria: boolean;
+ network: string;
onRemove: () => void;
}) {
- const path = [...base, 'udp', index];
- const type = Form.useWatch([...path, 'type'], form) as string | undefined;
- const network = Form.useWatch([...base.slice(0, -1), 'network'], form) as string | undefined;
+ const absolutePath = [...listPath, fieldName];
const onTypeChange = (v: string) => {
- form.setFieldValue([...path, 'settings'], defaultUdpMaskSettings(v));
+ form.setFieldValue([...absolutePath, 'settings'], defaultUdpMaskSettings(v));
if (network === 'kcp') {
- const kcpPath = [...base.slice(0, -1), 'kcpSettings', 'mtu'];
- form.setFieldValue(kcpPath, v === 'xdns' ? 900 : 1350);
+ const kcpMtuPath = [...listPath.slice(0, -1), 'kcpSettings', 'mtu'];
+ form.setFieldValue(kcpMtuPath, v === 'xdns' ? 900 : 1350);
}
};
@@ -367,55 +402,85 @@ function UdpMaskItem({
-
+
- {(type === 'mkcp-aes128gcm' || type === 'salamander') && (
-
-
-
- )}
-
- {type === 'header-dns' && (
-
-
-
- )}
-
- {type === 'xdns' && (
-
-
-
- )}
-
- {type === 'xicmp' && (
- <>
-
-
-
-
-
-
- >
- )}
-
- {type === 'header-custom' && (
-
- )}
-
- {type === 'noise' && (
-
- )}
+ getDeep(prev, [...absolutePath, 'type']) !== getDeep(curr, [...absolutePath, 'type'])}
+ >
+ {({ getFieldValue }) => {
+ const type = getFieldValue([...absolutePath, 'type']) as string | undefined;
+ if (type === 'mkcp-aes128gcm' || type === 'salamander') {
+ return (
+
+
+
+ );
+ }
+ if (type === 'header-dns') {
+ return (
+
+
+
+ );
+ }
+ if (type === 'xdns') {
+ return (
+
+
+
+ );
+ }
+ if (type === 'xicmp') {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+ }
+ if (type === 'header-custom') {
+ return (
+
+ );
+ }
+ if (type === 'noise') {
+ return (
+
+ );
+ }
+ return null;
+ }}
+
);
}
-function UdpHeaderCustom({ base, form }: { base: (string | number)[]; form: FormInstance }) {
+function UdpHeaderCustom({
+ udpFieldName, form, absoluteSettingsPath,
+}: {
+ udpFieldName: number;
+ form: FormInstance;
+ absoluteSettingsPath: (string | number)[];
+}) {
return (
<>
{(['client', 'server'] as const).map((groupKey) => (
-
+
{(items, { add, remove }) => (
<>
@@ -433,8 +498,9 @@ function UdpHeaderCustom({ base, form }: { base: (string | number)[]; form: Form
remove(item.name)} />
remove(item.name)}
/>
@@ -447,13 +513,19 @@ function UdpHeaderCustom({ base, form }: { base: (string | number)[]; form: Form
);
}
-function NoiseItems({ base, form }: { base: (string | number)[]; form: FormInstance }) {
+function NoiseItems({
+ udpFieldName, form, absoluteSettingsPath,
+}: {
+ udpFieldName: number;
+ form: FormInstance;
+ absoluteSettingsPath: (string | number)[];
+}) {
return (
<>
-
+
-
+
{(items, { add, remove }) => (
<>
@@ -471,8 +543,9 @@ function NoiseItems({ base, form }: { base: (string | number)[]; form: FormInsta
remove(item.name)} />
remove(item.name)}
/>
@@ -486,28 +559,28 @@ function NoiseItems({ base, form }: { base: (string | number)[]; form: FormInsta
}
function ItemEditor({
- base, form, delayMode, onRemove: _onRemove,
+ fieldName, form, absoluteItemPath, delayMode, onRemove: _onRemove,
}: {
- base: (string | number)[];
+ fieldName: number;
form: FormInstance;
+ absoluteItemPath: (string | number)[];
delayMode?: 'number' | 'string';
onRemove?: () => void;
}) {
- const type = Form.useWatch([...base, 'type'], form) as string | undefined;
-
const onTypeChange = (v: string) => {
- if (v === 'base64') form.setFieldValue([...base, 'packet'], RandomUtil.randomBase64());
- else if (v === 'array') {
- form.setFieldValue([...base, 'rand'], delayMode === 'string' ? '1-8192' : 0);
- form.setFieldValue([...base, 'packet'], []);
+ if (v === 'base64') {
+ form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64());
+ } else if (v === 'array') {
+ form.setFieldValue([...absoluteItemPath, 'rand'], delayMode === 'string' ? '1-8192' : 0);
+ form.setFieldValue([...absoluteItemPath, 'packet'], []);
} else {
- form.setFieldValue([...base, 'packet'], '');
+ form.setFieldValue([...absoluteItemPath, 'packet'], '');
}
};
return (
<>
-
+
{delayMode === 'number' && (
-
+
)}
{delayMode === 'string' && (
-
+
)}
- {type === 'array' ? (
- <>
-
- {delayMode === 'string' ? (
-
- ) : (
-
- )}
-
-
-
-
- >
- ) : type === 'base64' ? (
-
-
-
-
+ getDeep(prev, [...absoluteItemPath, 'type']) !== getDeep(curr, [...absoluteItemPath, 'type'])}
+ >
+ {({ getFieldValue }) => {
+ const type = getFieldValue([...absoluteItemPath, 'type']) as string | undefined;
+ if (type === 'array') {
+ return (
+ <>
+
+ {delayMode === 'string' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ );
+ }
+ if (type === 'base64') {
+ return (
+
+
+
+
+
+ }
+ onClick={() => form.setFieldValue([...absoluteItemPath, 'packet'], RandomUtil.randomBase64())}
+ />
+
+
+ );
+ }
+ return (
+
+
- }
- onClick={() => form.setFieldValue([...base, 'packet'], RandomUtil.randomBase64())}
- />
-
-
- ) : (
-
-
-
- )}
+ );
+ }}
+
>
);
}
diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx
index 61f3d530..91f093fb 100644
--- a/frontend/src/pages/inbounds/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/InboundFormModal.tsx
@@ -261,7 +261,11 @@ function buildAddModeValues(): InboundFormValues {
return rawInboundToFormValues({
protocol: 'vless',
settings,
- streamSettings: { network: 'tcp', security: 'none' },
+ streamSettings: {
+ network: 'tcp',
+ security: 'none',
+ tcpSettings: { header: { type: 'none' } },
+ },
sniffing: SniffingSchema.parse({}),
port: RandomUtil.randomInteger(10000, 60000),
listen: '',
@@ -1296,10 +1300,36 @@ export default function InboundFormModal({
>
);
- // Switching `network` swaps which per-network key (tcpSettings, wsSettings,
- // grpcSettings, ...) appears on the wire. We clear the previously selected
- // network's settings blob and seed a default empty object for the new one
- // so AntD's Form.Items aren't pointed at undefined nested paths.
+ // Switching `network` swaps which per-network key (tcpSettings,
+ // wsSettings, grpcSettings, ...) appears on the wire. Clear the old
+ // network's blob and seed the new one with the schema defaults so the
+ // Form.Items inside it have valid initial values (KCP needs MTU=1350
+ // etc., not empty strings).
+ const newStreamSlice = (n: string): Record => {
+ switch (n) {
+ case 'tcp':
+ return { header: { type: 'none' } };
+ case 'kcp':
+ return {
+ mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
+ congestion: false, readBufferSize: 2, writeBufferSize: 2,
+ header: { type: 'none' }, seed: '',
+ };
+ case 'ws':
+ return { path: '/', host: '', headers: {}, heartbeatPeriod: 0 };
+ case 'grpc':
+ return { serviceName: '', authority: '', multiMode: false };
+ case 'httpupgrade':
+ return { path: '/', host: '', headers: {} };
+ case 'xhttp':
+ return {
+ path: '/', host: '', mode: 'auto', headers: {},
+ xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
+ };
+ default:
+ return {};
+ }
+ };
const onNetworkChange = (next: string) => {
const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings'];
const current = (form.getFieldValue('streamSettings') as Record) ?? {};
@@ -1307,7 +1337,7 @@ export default function InboundFormModal({
for (const k of ALL) {
if (k !== `${next}Settings`) delete cleaned[k];
}
- cleaned[`${next}Settings`] = {};
+ cleaned[`${next}Settings`] = newStreamSlice(next);
form.setFieldValue('streamSettings', cleaned);
};