diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx
index f8364ff9..018c1970 100644
--- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx
+++ b/frontend/src/pages/inbounds/form/InboundFormModal.tsx
@@ -22,7 +22,6 @@ import {
ArrowDownOutlined,
ArrowUpOutlined,
DeleteOutlined,
- MinusOutlined,
PlusOutlined,
} from '@ant-design/icons';
@@ -47,18 +46,10 @@ import {
} from '@/schemas/forms/inbound-form';
import { antdRule } from '@/utils/zodForm';
import {
- ALPN_OPTION,
- Address_Port_Strategy,
- DOMAIN_STRATEGY_OPTION,
Protocols,
SNIFFING_OPTION,
- TCP_CONGESTION_OPTION,
- UTLS_FINGERPRINT,
} from '@/schemas/primitives';
-import {
- HappyEyeballsSchema,
- SockoptStreamSettingsSchema,
-} from '@/schemas/protocols/stream/sockopt';
+import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
@@ -86,10 +77,12 @@ import {
WireguardFields,
} from './protocols';
import {
+ ExternalProxyForm,
GrpcForm,
HttpUpgradeForm,
KcpForm,
RawForm,
+ SockoptForm,
WsForm,
XhttpForm,
} from './transport';
@@ -1058,378 +1051,9 @@ export default function InboundFormModal({
{network === 'kcp' && }
-
{
- const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
- const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
- return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
- }}
- >
- {({ getFieldValue }) => {
- const arr = getFieldValue(['streamSettings', 'externalProxy']);
- const on = Array.isArray(arr) && arr.length > 0;
- return (
- <>
-
-
-
- {on && (
-
- {(fields, { add, remove }) => (
- <>
-
-
-
-
- {fields.map((field) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- remove(field.name)}>
-
-
-
-
- prev.streamSettings?.externalProxy?.[field.name]?.forceTls
- !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
- }
- >
- {({ getFieldValue }) => {
- const ft = getFieldValue([
- 'streamSettings', 'externalProxy', field.name, 'forceTls',
- ]);
- if (ft !== 'tls') return null;
- return (
-
-
-
-
-
-
-
-
-
- );
- }}
-
-
- ))}
-
- >
- )}
-
- )}
- >
- );
- }}
-
+
- {
- const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
- const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
- return !!a !== !!b;
- }}
- >
- {({ getFieldValue }) => {
- const sock = getFieldValue(['streamSettings', 'sockopt']);
- const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
- return (
- <>
-
-
-
- {on && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {({ getFieldValue, setFieldValue }) => {
- const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
- const hasHe = he != null;
- return (
- <>
-
- {
- setFieldValue(
- ['streamSettings', 'sockopt', 'happyEyeballs'],
- v ? HappyEyeballsSchema.parse({}) : undefined,
- );
- }}
- />
-
- {hasHe && (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
- >
- );
- }}
-
-
- {(fields, { add, remove }) => (
- <>
-
-
-
- {fields.map((field) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
- >
- )}
-
- >
- )}
- >
- );
- }}
-
+
void;
+}) {
+ const { t } = useTranslation();
+ return (
+ {
+ const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
+ const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
+ return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
+ }}
+ >
+ {({ getFieldValue }) => {
+ const arr = getFieldValue(['streamSettings', 'externalProxy']);
+ const on = Array.isArray(arr) && arr.length > 0;
+ return (
+ <>
+
+
+
+ {on && (
+
+ {(fields, { add, remove }) => (
+ <>
+
+
+
+
+ {fields.map((field) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ remove(field.name)}>
+
+
+
+
+ prev.streamSettings?.externalProxy?.[field.name]?.forceTls
+ !== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
+ }
+ >
+ {({ getFieldValue }) => {
+ const ft = getFieldValue([
+ 'streamSettings', 'externalProxy', field.name, 'forceTls',
+ ]);
+ if (ft !== 'tls') return null;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }}
+
+
+ ))}
+
+ >
+ )}
+
+ )}
+ >
+ );
+ }}
+
+ );
+}
diff --git a/frontend/src/pages/inbounds/form/transport/index.ts b/frontend/src/pages/inbounds/form/transport/index.ts
index ebcfb747..b582c45c 100644
--- a/frontend/src/pages/inbounds/form/transport/index.ts
+++ b/frontend/src/pages/inbounds/form/transport/index.ts
@@ -4,3 +4,5 @@ export { default as GrpcForm } from './grpc';
export { default as XhttpForm } from './xhttp';
export { default as HttpUpgradeForm } from './httpupgrade';
export { default as KcpForm } from './kcp';
+export { default as ExternalProxyForm } from './external-proxy';
+export { default as SockoptForm } from './sockopt';
diff --git a/frontend/src/pages/inbounds/form/transport/sockopt.tsx b/frontend/src/pages/inbounds/form/transport/sockopt.tsx
new file mode 100644
index 00000000..f139f1ee
--- /dev/null
+++ b/frontend/src/pages/inbounds/form/transport/sockopt.tsx
@@ -0,0 +1,270 @@
+import { useTranslation } from 'react-i18next';
+import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+
+import {
+ Address_Port_Strategy,
+ DOMAIN_STRATEGY_OPTION,
+ TCP_CONGESTION_OPTION,
+} from '@/schemas/primitives';
+import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
+
+export default function SockoptForm({
+ toggleSockopt,
+}: {
+ toggleSockopt: (on: boolean) => void;
+}) {
+ const { t } = useTranslation();
+ return (
+ {
+ const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
+ const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
+ return !!a !== !!b;
+ }}
+ >
+ {({ getFieldValue }) => {
+ const sock = getFieldValue(['streamSettings', 'sockopt']);
+ const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
+ return (
+ <>
+
+
+
+ {on && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {({ getFieldValue, setFieldValue }) => {
+ const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
+ const hasHe = he != null;
+ return (
+ <>
+
+ {
+ setFieldValue(
+ ['streamSettings', 'sockopt', 'happyEyeballs'],
+ v ? HappyEyeballsSchema.parse({}) : undefined,
+ );
+ }}
+ />
+
+ {hasHe && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ );
+ }}
+
+
+ {(fields, { add, remove }) => (
+ <>
+
+
+
+ {fields.map((field) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+ >
+ )}
+
+ >
+ )}
+ >
+ );
+ }}
+
+ );
+}
diff --git a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
index b5f850da..53facfe2 100644
--- a/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
+++ b/frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
@@ -36,6 +36,12 @@ exports[`inbound security forms > TlsForm field structure is stable 1`] = `
]
`;
+exports[`inbound transport forms > ExternalProxyForm field structure is stable (one TLS entry) 1`] = `
+[
+ "External Proxy",
+]
+`;
+
exports[`inbound transport forms > GrpcForm field structure is stable 1`] = `
[
"Service Name",
@@ -71,6 +77,36 @@ exports[`inbound transport forms > RawForm field structure is stable 1`] = `
]
`;
+exports[`inbound transport forms > SockoptForm field structure is stable (enabled + happy eyeballs) 1`] = `
+[
+ "Sockopt",
+ "Route Mark",
+ "TCP Keep Alive Interval",
+ "TCP Keep Alive Idle",
+ "TCP Max Seg",
+ "TCP User Timeout",
+ "TCP Window Clamp",
+ "Proxy Protocol",
+ "TCP Fast Open",
+ "Multipath TCP",
+ "Penetrate",
+ "V6 Only",
+ "Domain Strategy",
+ "TCP Congestion",
+ "TProxy",
+ "Dialer Proxy",
+ "Interface name",
+ "Trusted X-Forwarded-For",
+ "Address+port strategy",
+ "Happy Eyeballs",
+ "Try delay (ms)",
+ "Prioritize IPv6",
+ "Interleave",
+ "Max concurrent try",
+ "Custom sockopt",
+]
+`;
+
exports[`inbound transport forms > WsForm field structure is stable 1`] = `
[
"Proxy Protocol",
diff --git a/frontend/src/test/inbound-form-blocks.test.tsx b/frontend/src/test/inbound-form-blocks.test.tsx
index 15d376f2..7b523575 100644
--- a/frontend/src/test/inbound-form-blocks.test.tsx
+++ b/frontend/src/test/inbound-form-blocks.test.tsx
@@ -3,10 +3,12 @@ import { Form, type FormInstance } from 'antd';
import type { ReactNode } from 'react';
import {
+ ExternalProxyForm,
GrpcForm,
HttpUpgradeForm,
KcpForm,
RawForm,
+ SockoptForm,
WsForm,
XhttpForm,
} from '@/pages/inbounds/form/transport';
@@ -14,13 +16,22 @@ import { RealityForm, TlsForm } from '@/pages/inbounds/form/security';
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
import { renderWithProviders, fieldLabels } from './test-utils';
-function FormHarness({ children }: { children: (form: FormInstance) => ReactNode }) {
+function FormHarness({
+ children,
+ initialValues,
+}: {
+ children: (form: FormInstance) => ReactNode;
+ initialValues?: Record;
+}) {
const [form] = Form.useForm();
- return ;
+ return ;
}
-function renderInForm(node: (form: FormInstance) => ReactNode) {
- return renderWithProviders({node});
+function renderInForm(
+ node: (form: FormInstance) => ReactNode,
+ initialValues?: Record,
+) {
+ return renderWithProviders({node});
}
const noop = () => {};
@@ -55,6 +66,34 @@ describe('inbound transport forms', () => {
renderInForm((form) => );
expect(fieldLabels()).toMatchSnapshot();
});
+
+ it('ExternalProxyForm field structure is stable (one TLS entry)', () => {
+ renderInForm(
+ () => ,
+ {
+ streamSettings: {
+ externalProxy: [{
+ forceTls: 'tls',
+ dest: '',
+ port: 443,
+ remark: '',
+ sni: '',
+ fingerprint: '',
+ alpn: [],
+ }],
+ },
+ },
+ );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
+
+ it('SockoptForm field structure is stable (enabled + happy eyeballs)', () => {
+ renderInForm(
+ () => ,
+ { streamSettings: { sockopt: { happyEyeballs: {} } } },
+ );
+ expect(fieldLabels()).toMatchSnapshot();
+ });
});
describe('inbound security forms', () => {