OutboundFormModal.tsx 110 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241
  1. import { useEffect, useMemo, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import {
  4. Button,
  5. Form,
  6. Input,
  7. InputNumber,
  8. Modal,
  9. Radio,
  10. Select,
  11. Space,
  12. Switch,
  13. Tabs,
  14. message,
  15. } from 'antd';
  16. import { DeleteOutlined, MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
  17. import FinalMaskForm from '@/components/FinalMaskForm';
  18. import HeaderMapEditor from '@/components/HeaderMapEditor';
  19. import InputAddon from '@/components/InputAddon';
  20. import JsonEditor from '@/components/JsonEditor';
  21. import { Wireguard } from '@/utils';
  22. import {
  23. XMUX_DEFAULTS,
  24. formValuesToWirePayload,
  25. rawOutboundToFormValues,
  26. } from '@/lib/xray/outbound-form-adapter';
  27. import { parseOutboundLink } from '@/lib/xray/outbound-link-parser';
  28. import {
  29. OutboundFormBaseSchema,
  30. ShadowsocksOutboundFormSettingsSchema,
  31. TrojanOutboundFormSettingsSchema,
  32. VlessOutboundFormSettingsSchema,
  33. VmessOutboundFormSettingsSchema,
  34. type OutboundFormValues,
  35. } from '@/schemas/forms/outbound-form';
  36. import {
  37. ALPN_OPTION,
  38. Address_Port_Strategy,
  39. DNSRuleActions,
  40. DOMAIN_STRATEGY_OPTION,
  41. MODE_OPTION,
  42. OutboundDomainStrategies,
  43. OutboundProtocols as Protocols,
  44. SNIFFING_OPTION,
  45. TCP_CONGESTION_OPTION,
  46. TLS_FLOW_CONTROL,
  47. USERS_SECURITY,
  48. UTLS_FINGERPRINT,
  49. WireguardDomainStrategy,
  50. } from '@/schemas/primitives';
  51. import {
  52. HappyEyeballsSchema,
  53. SockoptStreamSettingsSchema,
  54. } from '@/schemas/protocols/stream/sockopt';
  55. import {
  56. canEnableReality,
  57. canEnableStream,
  58. canEnableTls,
  59. canEnableTlsFlow,
  60. } from '@/lib/xray/protocol-capabilities';
  61. import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
  62. import { antdRule } from '@/utils/zodForm';
  63. import './OutboundFormModal.css';
  64. // Pattern A rewrite of OutboundFormModal. Built as a sibling `.new.tsx`
  65. // file so the build stays green section-by-section. The atomic swap at
  66. // the end of the rewrite replaces the legacy file in one commit
  67. // (per Core Decision 7 in the migration spec).
  68. interface OutboundFormModalProps {
  69. open: boolean;
  70. outbound: Record<string, unknown> | null;
  71. existingTags: string[];
  72. onClose: () => void;
  73. onConfirm: (outbound: Record<string, unknown>) => void;
  74. }
  75. const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label: p }));
  76. const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, label: v }));
  77. const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v }));
  78. const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v }));
  79. const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v }));
  80. const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v }));
  81. const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v }));
  82. const ADDRESS_PORT_STRATEGY_OPTIONS = Object.values(Address_Port_Strategy).map((v) => ({
  83. value: v,
  84. label: v,
  85. }));
  86. // canEnableMux mirrors the adapter's helper but lives here so the modal
  87. // can show/hide the Mux section without going through the adapter.
  88. const MUX_PROTOCOLS = new Set<string>(['vmess', 'vless', 'trojan', 'shadowsocks', 'http', 'socks']);
  89. function isMuxAllowed(protocol: string, flow: string, network: string): boolean {
  90. if (!MUX_PROTOCOLS.has(protocol)) return false;
  91. if (protocol === 'vless' && flow) return false;
  92. if (network === 'xhttp') return false;
  93. return true;
  94. }
  95. const NETWORK_OPTIONS: { value: string; label: string }[] = [
  96. { value: 'tcp', label: 'RAW' },
  97. { value: 'kcp', label: 'mKCP' },
  98. { value: 'ws', label: 'WebSocket' },
  99. { value: 'grpc', label: 'gRPC' },
  100. { value: 'httpupgrade', label: 'HTTPUpgrade' },
  101. { value: 'xhttp', label: 'XHTTP' },
  102. ];
  103. // Hysteria appends an extra `hysteria` network branch to the selector
  104. // — only when the parent protocol is hysteria. Wire-side this matches
  105. // the legacy modal's `isHysteria ? [...NETWORKS, 'hysteria'] : NETWORKS`.
  106. const HYSTERIA_NETWORK_OPTION = { value: 'hysteria', label: 'Hysteria' };
  107. // Per-network bootstrap. Mirrors the legacy class constructors so the
  108. // initial state for each transport matches what xray-core expects.
  109. function newStreamSlice(network: string): Record<string, unknown> {
  110. switch (network) {
  111. case 'tcp':
  112. return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
  113. case 'kcp':
  114. return {
  115. network: 'kcp',
  116. kcpSettings: {
  117. mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
  118. cwndMultiplier: 1, maxSendingWindow: 2097152,
  119. },
  120. };
  121. case 'ws':
  122. return {
  123. network: 'ws',
  124. wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
  125. };
  126. case 'grpc':
  127. return {
  128. network: 'grpc',
  129. grpcSettings: { serviceName: '', authority: '', multiMode: false },
  130. };
  131. case 'httpupgrade':
  132. return {
  133. network: 'httpupgrade',
  134. httpupgradeSettings: { path: '/', host: '', headers: {} },
  135. };
  136. case 'xhttp':
  137. return {
  138. network: 'xhttp',
  139. xhttpSettings: {
  140. path: '/', host: '', mode: '', headers: [],
  141. xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
  142. },
  143. };
  144. case 'hysteria':
  145. return {
  146. network: 'hysteria',
  147. hysteriaSettings: {
  148. version: 2,
  149. auth: '',
  150. udpIdleTimeout: 60,
  151. },
  152. };
  153. default:
  154. return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
  155. }
  156. }
  157. // Protocols whose form schema carries a flat connect target — these all
  158. // get the shared "server" sub-block (address + port) at the top of the
  159. // protocol section. Wireguard has an address but no port. DNS/freedom/
  160. // blackhole/loopback have no connect target.
  161. const SERVER_PROTOCOLS = new Set<string>([
  162. 'vmess', 'vless', 'trojan', 'shadowsocks', 'socks', 'http', 'hysteria',
  163. ]);
  164. function buildAddModeValues(): OutboundFormValues {
  165. return rawOutboundToFormValues({});
  166. }
  167. export default function OutboundFormModal({
  168. open,
  169. outbound: outboundProp,
  170. existingTags,
  171. onClose,
  172. onConfirm,
  173. }: OutboundFormModalProps) {
  174. const { t } = useTranslation();
  175. const [messageApi, messageContextHolder] = message.useMessage();
  176. const [form] = Form.useForm<OutboundFormValues>();
  177. const [activeKey, setActiveKey] = useState('1');
  178. const [jsonText, setJsonText] = useState('');
  179. const [jsonDirty, setJsonDirty] = useState(false);
  180. const [linkInput, setLinkInput] = useState('');
  181. // Parse a share link (vmess:// / vless:// / trojan:// / ss:// /
  182. // hysteria2://) and replace form state with the result. The current
  183. // tag is preserved when the parsed link doesn't carry one.
  184. function importLink() {
  185. const link = linkInput.trim();
  186. if (!link) return;
  187. const parsed = parseOutboundLink(link);
  188. if (!parsed) {
  189. messageApi.error('Wrong Link!');
  190. return;
  191. }
  192. const currentTag = form.getFieldValue('tag') as string | undefined;
  193. if (!parsed.tag && currentTag) parsed.tag = currentTag;
  194. const next = rawOutboundToFormValues(parsed);
  195. form.resetFields();
  196. form.setFieldsValue(next);
  197. setJsonText(JSON.stringify(formValuesToWirePayload(next), null, 2));
  198. setJsonDirty(false);
  199. setLinkInput('');
  200. messageApi.success('Link imported successfully');
  201. switchTab('1');
  202. }
  203. const isEdit = outboundProp != null;
  204. const title = isEdit
  205. ? `${t('edit')} ${t('pages.xray.Outbounds')}`
  206. : `+ ${t('pages.xray.Outbounds')}`;
  207. const okText = isEdit ? t('pages.clients.submitEdit') : t('create');
  208. useEffect(() => {
  209. if (!open) return;
  210. const initial = outboundProp
  211. ? rawOutboundToFormValues(outboundProp)
  212. : buildAddModeValues();
  213. form.resetFields();
  214. form.setFieldsValue(initial);
  215. setActiveKey('1');
  216. setJsonText(JSON.stringify(formValuesToWirePayload(initial), null, 2));
  217. setJsonDirty(false);
  218. }, [open, outboundProp, form]);
  219. const tag = Form.useWatch('tag', form) ?? '';
  220. const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
  221. // preserve: true — without it useWatch only reflects values whose
  222. // Form.Item is currently mounted. The streamSettings selectors live
  223. // INSIDE `{streamAllowed && network && (...)}`, so the moment that
  224. // conditional gates them out, useWatch returns undefined, the gate
  225. // keeps returning false, and the stream block never renders even
  226. // though streamSettings is in the form store.
  227. const network = (Form.useWatch(['streamSettings', 'network'], { form, preserve: true }) ?? '') as string;
  228. const security = (Form.useWatch(['streamSettings', 'security'], { form, preserve: true }) ?? 'none') as string;
  229. const streamAllowed = canEnableStream({ protocol });
  230. const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
  231. const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
  232. const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
  233. // Seed streamSettings when the user picks a protocol that supports
  234. // streams but the form does not yet have a stream slice (new outbound,
  235. // or wire payload arrived without streamSettings).
  236. useEffect(() => {
  237. if (!streamAllowed) return;
  238. if (network) return;
  239. form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
  240. // eslint-disable-next-line react-hooks/exhaustive-deps
  241. }, [streamAllowed, network]);
  242. // Wireguard pubKey is a UI-only field derived from secretKey on every
  243. // edit. The legacy modal did the same on every keystroke. We re-derive
  244. // here so paste-in secret keys immediately surface the matching pub.
  245. const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form) as string | undefined;
  246. useEffect(() => {
  247. if (protocol !== 'wireguard') return;
  248. const sk = (wgSecretKey ?? '').trim();
  249. if (!sk) {
  250. form.setFieldValue(['settings', 'pubKey'], '');
  251. return;
  252. }
  253. try {
  254. const { publicKey } = Wireguard.generateKeypair(sk);
  255. form.setFieldValue(['settings', 'pubKey'], publicKey);
  256. } catch {
  257. form.setFieldValue(['settings', 'pubKey'], '');
  258. }
  259. // eslint-disable-next-line react-hooks/exhaustive-deps
  260. }, [protocol, wgSecretKey]);
  261. // Switching protocol resets the settings sub-object to fresh defaults
  262. // so leftover fields from the previous protocol do not bleed through.
  263. // The adapter's rawOutboundToFormValues seeds whatever the new protocol
  264. // expects (vless flat shape, vmess flat shape, wireguard with secretKey
  265. // placeholder, etc.).
  266. function onValuesChange(changed: Partial<OutboundFormValues>) {
  267. if ('protocol' in changed && changed.protocol) {
  268. const next = rawOutboundToFormValues({ protocol: changed.protocol });
  269. form.setFieldValue('settings', next.settings);
  270. }
  271. }
  272. // Security change cascade: swap the security sub-key so the DU branch
  273. // matches. Seed default field values when entering tls/reality so the
  274. // sub-forms render without `undefined` field references.
  275. function onSecurityChange(next: string) {
  276. const stream = form.getFieldValue('streamSettings') ?? {};
  277. const cleaned = { ...stream } as Record<string, unknown>;
  278. delete cleaned.tlsSettings;
  279. delete cleaned.realitySettings;
  280. if (next === 'tls') {
  281. cleaned.tlsSettings = {
  282. serverName: '',
  283. alpn: [],
  284. fingerprint: '',
  285. echConfigList: '',
  286. verifyPeerCertByName: '',
  287. pinnedPeerCertSha256: '',
  288. };
  289. } else if (next === 'reality') {
  290. cleaned.realitySettings = {
  291. publicKey: '',
  292. fingerprint: 'chrome',
  293. serverName: '',
  294. shortId: '',
  295. spiderX: '',
  296. mldsa65Verify: '',
  297. };
  298. }
  299. cleaned.security = next;
  300. form.setFieldValue('streamSettings', cleaned);
  301. }
  302. // Network change cascade: swap the per-network sub-key (tcpSettings,
  303. // wsSettings, etc.) so the DU branch matches. Preserve security if
  304. // the new network supports it, otherwise force back to 'none'.
  305. function onNetworkChange(next: string) {
  306. const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
  307. const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
  308. const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
  309. const newSecurity =
  310. currentSecurity === 'tls' && !stillAllowed
  311. ? 'none'
  312. : currentSecurity === 'reality' && !stillReality
  313. ? 'none'
  314. : currentSecurity;
  315. form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
  316. }
  317. function onXmuxToggle(checked: boolean) {
  318. if (!checked) return;
  319. const existing = form.getFieldValue(['streamSettings', 'xhttpSettings', 'xmux']);
  320. const hasValues = existing && typeof existing === 'object' && Object.keys(existing).length > 0;
  321. if (hasValues) return;
  322. form.setFieldValue(['streamSettings', 'xhttpSettings', 'xmux'], { ...XMUX_DEFAULTS });
  323. }
  324. const duplicateTag = useMemo(() => {
  325. const myTag = tag.trim();
  326. if (!myTag) return false;
  327. if (isEdit && (outboundProp?.tag as string | undefined) === myTag) return false;
  328. return (existingTags || []).includes(myTag);
  329. }, [tag, existingTags, isEdit, outboundProp]);
  330. // Bridge form ↔ JSON tab: when leaving the JSON tab back to Basic, push
  331. // any edits into form state. When entering JSON tab, snapshot current
  332. // form values so the user sees the live shape.
  333. function applyJsonToForm(): boolean {
  334. if (!jsonDirty) return true;
  335. const raw = jsonText.trim();
  336. if (!raw) return true;
  337. let parsed: Record<string, unknown>;
  338. try {
  339. parsed = JSON.parse(raw) as Record<string, unknown>;
  340. } catch (e) {
  341. messageApi.error(`JSON: ${(e as Error).message}`);
  342. return false;
  343. }
  344. const next = rawOutboundToFormValues(parsed);
  345. form.resetFields();
  346. form.setFieldsValue(next);
  347. setJsonDirty(false);
  348. return true;
  349. }
  350. // Wrap every tab switch with a blur of the active element. AntD marks
  351. // the outgoing panel `aria-hidden="true"` synchronously when the
  352. // controlled activeKey flips; if a focused input is still inside that
  353. // panel (e.g. Input.Search on the JSON tab after user hits Enter to
  354. // import), Chrome logs a WAI-ARIA warning. Doing the blur right
  355. // before setActiveKey ensures the panel is unfocused by the time
  356. // AntD applies the attribute.
  357. function switchTab(key: string) {
  358. if (typeof document !== 'undefined') {
  359. (document.activeElement as HTMLElement | null)?.blur?.();
  360. }
  361. setActiveKey(key);
  362. }
  363. function onTabChange(key: string) {
  364. if (key === '2') {
  365. const values = form.getFieldsValue(true) as OutboundFormValues;
  366. setJsonText(JSON.stringify(formValuesToWirePayload(values), null, 2));
  367. setJsonDirty(false);
  368. switchTab(key);
  369. return;
  370. }
  371. if (key === '1' && activeKey === '2') {
  372. if (!applyJsonToForm()) return;
  373. }
  374. switchTab(key);
  375. }
  376. async function onOk() {
  377. let values: OutboundFormValues;
  378. if (activeKey === '2') {
  379. const raw = jsonText.trim();
  380. if (!raw) return;
  381. let parsed: Record<string, unknown>;
  382. try {
  383. parsed = JSON.parse(raw) as Record<string, unknown>;
  384. } catch (e) {
  385. messageApi.error(`JSON: ${(e as Error).message}`);
  386. return;
  387. }
  388. values = rawOutboundToFormValues(parsed);
  389. form.resetFields();
  390. form.setFieldsValue(values);
  391. setJsonDirty(false);
  392. } else {
  393. try {
  394. await form.validateFields();
  395. } catch {
  396. return;
  397. }
  398. values = form.getFieldsValue(true) as OutboundFormValues;
  399. }
  400. const tagValue = (values.tag ?? '').trim();
  401. if (!tagValue) {
  402. messageApi.error(t('pages.xray.outboundForm.tagRequired'));
  403. return;
  404. }
  405. const isDuplicateTag = (existingTags || []).includes(tagValue)
  406. && !(isEdit && (outboundProp?.tag as string | undefined) === tagValue);
  407. if (isDuplicateTag) {
  408. messageApi.error('Tag already used by another outbound');
  409. return;
  410. }
  411. onConfirm(formValuesToWirePayload(values));
  412. }
  413. return (
  414. <>
  415. {messageContextHolder}
  416. <Modal
  417. open={open}
  418. title={title}
  419. okText={okText}
  420. cancelText={t('close')}
  421. mask={{ closable: false }}
  422. width={780}
  423. onOk={onOk}
  424. onCancel={onClose}
  425. destroyOnHidden
  426. >
  427. <Form
  428. form={form}
  429. colon={false}
  430. labelCol={{ md: { span: 8 } }}
  431. wrapperCol={{ md: { span: 14 } }}
  432. onValuesChange={onValuesChange}
  433. >
  434. <Tabs
  435. activeKey={activeKey}
  436. onChange={onTabChange}
  437. items={[
  438. {
  439. key: '1',
  440. label: t('pages.xray.basicTemplate'),
  441. children: (
  442. <>
  443. <Form.Item
  444. label={t('protocol')}
  445. name="protocol"
  446. rules={[antdRule(OutboundFormBaseSchema.shape.tag, t)]}
  447. >
  448. <Select options={PROTOCOL_OPTIONS} />
  449. </Form.Item>
  450. <Form.Item
  451. label={t('pages.xray.outbound.tag')}
  452. name="tag"
  453. validateStatus={duplicateTag ? 'warning' : undefined}
  454. help={duplicateTag ? t('pages.xray.outboundForm.tagDuplicate') : undefined}
  455. rules={[
  456. { required: true, message: t('pages.xray.outboundForm.tagRequired') },
  457. ]}
  458. >
  459. <Input placeholder={t('pages.xray.outboundForm.tagPlaceholder')} />
  460. </Form.Item>
  461. <Form.Item label={t('pages.xray.outbound.sendThrough')} name="sendThrough">
  462. <Input placeholder={t('pages.xray.outboundForm.localIpPlaceholder')} />
  463. </Form.Item>
  464. {/* Shared connect target (address + port) for protocols
  465. whose form schema carries them flat at settings root.
  466. Hidden for freedom/blackhole/dns/loopback/wireguard. */}
  467. {SERVER_PROTOCOLS.has(protocol) && (
  468. <>
  469. <Form.Item
  470. label={t('pages.inbounds.address')}
  471. name={['settings', 'address']}
  472. rules={[{ required: true, message: t('pages.xray.outboundForm.addressRequired') }]}
  473. >
  474. <Input />
  475. </Form.Item>
  476. <Form.Item
  477. label={t('pages.inbounds.port')}
  478. name={['settings', 'port']}
  479. rules={[{ required: true, message: t('pages.xray.outboundForm.portRequired') }]}
  480. >
  481. <InputNumber min={1} max={65535} style={{ width: '100%' }} />
  482. </Form.Item>
  483. </>
  484. )}
  485. {(protocol === 'vmess' || protocol === 'vless') && (
  486. <Form.Item
  487. label="ID"
  488. name={['settings', 'id']}
  489. rules={[antdRule(VmessOutboundFormSettingsSchema.shape.id, t)]}
  490. >
  491. <Input placeholder="UUID" />
  492. </Form.Item>
  493. )}
  494. {protocol === 'vmess' && (
  495. <Form.Item
  496. label={t('security')}
  497. name={['settings', 'security']}
  498. rules={[antdRule(VmessOutboundFormSettingsSchema.shape.security, t)]}
  499. >
  500. <Select options={SECURITY_OPTIONS} />
  501. </Form.Item>
  502. )}
  503. {protocol === 'vless' && (
  504. <>
  505. <Form.Item
  506. label={t('encryption')}
  507. name={['settings', 'encryption']}
  508. rules={[antdRule(VlessOutboundFormSettingsSchema.shape.encryption, t)]}
  509. >
  510. <Input />
  511. </Form.Item>
  512. <Form.Item label={t('pages.clients.reverseTag')} name={['settings', 'reverseTag']}>
  513. <Input placeholder={t('pages.xray.outboundForm.optional')} />
  514. </Form.Item>
  515. </>
  516. )}
  517. {(protocol === 'trojan' || protocol === 'shadowsocks') && (
  518. <Form.Item
  519. label={t('password')}
  520. name={['settings', 'password']}
  521. rules={[
  522. antdRule(
  523. protocol === 'trojan'
  524. ? TrojanOutboundFormSettingsSchema.shape.password
  525. : ShadowsocksOutboundFormSettingsSchema.shape.password,
  526. t,
  527. ),
  528. ]}
  529. >
  530. <Input />
  531. </Form.Item>
  532. )}
  533. {protocol === 'shadowsocks' && (
  534. <>
  535. <Form.Item
  536. label={t('encryption')}
  537. name={['settings', 'method']}
  538. rules={[antdRule(SSMethodSchema, t)]}
  539. >
  540. <Select options={SS_METHOD_OPTIONS} />
  541. </Form.Item>
  542. <Form.Item
  543. label={t('pages.xray.outboundForm.udpOverTcp')}
  544. name={['settings', 'uot']}
  545. valuePropName="checked"
  546. >
  547. <Switch />
  548. </Form.Item>
  549. <Form.Item label={t('pages.xray.outboundForm.uotVersion')} name={['settings', 'UoTVersion']}>
  550. <InputNumber min={1} max={2} />
  551. </Form.Item>
  552. </>
  553. )}
  554. {(protocol === 'socks' || protocol === 'http') && (
  555. <>
  556. <Form.Item label={t('username')} name={['settings', 'user']}>
  557. <Input />
  558. </Form.Item>
  559. <Form.Item label={t('password')} name={['settings', 'pass']}>
  560. <Input />
  561. </Form.Item>
  562. </>
  563. )}
  564. {protocol === 'hysteria' && (
  565. <Form.Item label={t('pages.inbounds.form.version')} name={['settings', 'version']}>
  566. <InputNumber min={2} max={2} disabled />
  567. </Form.Item>
  568. )}
  569. {protocol === 'loopback' && (
  570. <Form.Item label={t('pages.xray.outboundForm.inboundTag')} name={['settings', 'inboundTag']}>
  571. <Input placeholder={t('pages.xray.outboundForm.inboundTagPlaceholder')} />
  572. </Form.Item>
  573. )}
  574. {protocol === 'blackhole' && (
  575. <Form.Item label={t('pages.xray.outboundForm.responseType')} name={['settings', 'type']}>
  576. <Select
  577. options={[
  578. { value: '', label: '(empty)' },
  579. { value: 'none', label: 'none' },
  580. { value: 'http', label: 'http' },
  581. ]}
  582. />
  583. </Form.Item>
  584. )}
  585. {protocol === 'dns' && (
  586. <>
  587. <Form.Item label={t('pages.xray.outboundForm.rewriteNetwork')} name={['settings', 'rewriteNetwork']}>
  588. <Select
  589. allowClear
  590. placeholder={t('pages.xray.outboundForm.unchanged')}
  591. options={[
  592. { value: 'udp', label: 'udp' },
  593. { value: 'tcp', label: 'tcp' },
  594. ]}
  595. />
  596. </Form.Item>
  597. <Form.Item label={t('pages.inbounds.form.rewriteAddress')} name={['settings', 'rewriteAddress']}>
  598. <Input placeholder={t('pages.xray.outboundForm.unchangedAddress')} />
  599. </Form.Item>
  600. <Form.Item label={t('pages.inbounds.form.rewritePort')} name={['settings', 'rewritePort']}>
  601. <InputNumber min={0} max={65535} style={{ width: '100%' }} />
  602. </Form.Item>
  603. <Form.Item label={t('pages.xray.tun.userLevel')} name={['settings', 'userLevel']}>
  604. <InputNumber min={0} style={{ width: '100%' }} />
  605. </Form.Item>
  606. <Form.List name={['settings', 'rules']}>
  607. {(fields, { add, remove }) => (
  608. <>
  609. <Form.Item label={t('pages.xray.outboundForm.rules')}>
  610. <Button
  611. size="small"
  612. type="primary"
  613. icon={<PlusOutlined />}
  614. onClick={() => add({ action: 'direct', qtype: '', domain: '' })}
  615. />
  616. </Form.Item>
  617. {fields.map((field, index) => (
  618. <div key={field.key}>
  619. <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
  620. <div className="item-heading">
  621. <span>{t('pages.xray.outboundForm.ruleN', { n: index + 1 })}</span>
  622. <DeleteOutlined
  623. className="danger-icon"
  624. onClick={() => remove(field.name)}
  625. />
  626. </div>
  627. </Form.Item>
  628. <Form.Item label={t('pages.xray.outboundForm.action')} name={[field.name, 'action']}>
  629. <Select
  630. options={DNSRuleActions.map((a) => ({ value: a, label: a }))}
  631. />
  632. </Form.Item>
  633. <Form.Item label="QType" name={[field.name, 'qtype']}>
  634. <Input placeholder="1,3,23-24" />
  635. </Form.Item>
  636. <Form.Item label={t('domainName')} name={[field.name, 'domain']}>
  637. <Input placeholder="domain:example.com" />
  638. </Form.Item>
  639. </div>
  640. ))}
  641. </>
  642. )}
  643. </Form.List>
  644. </>
  645. )}
  646. {protocol === 'freedom' && (
  647. <>
  648. <Form.Item label={t('pages.xray.balancer.balancerStrategy')} name={['settings', 'domainStrategy']}>
  649. <Select
  650. options={[
  651. { value: '', label: `(${t('none')})` },
  652. ...OutboundDomainStrategies.map((s) => ({ value: s, label: s })),
  653. ]}
  654. />
  655. </Form.Item>
  656. <Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
  657. <Input />
  658. </Form.Item>
  659. <Form.Item label={t('pages.xray.outboundForm.proxyProtocol')} name={['settings', 'proxyProtocol']}>
  660. <Select
  661. options={[
  662. { value: 0, label: `(${t('none')})` },
  663. { value: 1, label: 'v1' },
  664. { value: 2, label: 'v2' },
  665. ]}
  666. />
  667. </Form.Item>
  668. <Form.Item label={t('pages.xray.outboundForm.fragment')} shouldUpdate noStyle>
  669. {() => {
  670. const fragment = (form.getFieldValue(['settings', 'fragment']) ?? {}) as {
  671. packets?: string;
  672. length?: string;
  673. interval?: string;
  674. maxSplit?: string;
  675. };
  676. const enabled = !!(fragment.length || fragment.interval || fragment.maxSplit);
  677. return (
  678. <>
  679. <Form.Item label="Fragment">
  680. <Switch
  681. checked={enabled}
  682. onChange={(checked) => {
  683. form.setFieldValue(
  684. ['settings', 'fragment'],
  685. checked
  686. ? {
  687. packets: 'tlshello',
  688. length: '100-200',
  689. interval: '10-20',
  690. maxSplit: '300-400',
  691. }
  692. : { packets: '', length: '', interval: '', maxSplit: '' },
  693. );
  694. }}
  695. />
  696. </Form.Item>
  697. {enabled && (
  698. <>
  699. <Form.Item
  700. label={t('pages.settings.subFormats.packets')}
  701. name={['settings', 'fragment', 'packets']}
  702. >
  703. <Select
  704. options={[
  705. { value: '1-3', label: '1-3' },
  706. { value: 'tlshello', label: 'tlshello' },
  707. ]}
  708. />
  709. </Form.Item>
  710. <Form.Item label={t('pages.settings.subFormats.length')} name={['settings', 'fragment', 'length']}>
  711. <Input />
  712. </Form.Item>
  713. <Form.Item
  714. label={t('pages.settings.subFormats.interval')}
  715. name={['settings', 'fragment', 'interval']}
  716. >
  717. <Input />
  718. </Form.Item>
  719. <Form.Item
  720. label={t('pages.settings.subFormats.maxSplit')}
  721. name={['settings', 'fragment', 'maxSplit']}
  722. >
  723. <Input />
  724. </Form.Item>
  725. </>
  726. )}
  727. </>
  728. );
  729. }}
  730. </Form.Item>
  731. <Form.List name={['settings', 'noises']}>
  732. {(fields, { add, remove }) => (
  733. <>
  734. <Form.Item label={t('pages.settings.subFormats.noises')}>
  735. <Switch
  736. checked={fields.length > 0}
  737. onChange={(checked) => {
  738. if (checked) {
  739. add({
  740. type: 'rand',
  741. packet: '10-20',
  742. delay: '10-16',
  743. applyTo: 'ip',
  744. });
  745. } else {
  746. // remove() with no arg is not supported;
  747. // walk fields in reverse and drop each.
  748. for (let i = fields.length - 1; i >= 0; i--) {
  749. remove(fields[i].name);
  750. }
  751. }
  752. }}
  753. />
  754. {fields.length > 0 && (
  755. <Button
  756. size="small"
  757. type="primary"
  758. className="ml-8"
  759. icon={<PlusOutlined />}
  760. onClick={() =>
  761. add({
  762. type: 'rand',
  763. packet: '10-20',
  764. delay: '10-16',
  765. applyTo: 'ip',
  766. })
  767. }
  768. />
  769. )}
  770. </Form.Item>
  771. {fields.map((field, index) => (
  772. <div key={field.key}>
  773. <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
  774. <div className="item-heading">
  775. <span>{t('pages.settings.subFormats.noiseItem', { n: index + 1 })}</span>
  776. {fields.length > 1 && (
  777. <DeleteOutlined
  778. className="danger-icon"
  779. onClick={() => remove(field.name)}
  780. />
  781. )}
  782. </div>
  783. </Form.Item>
  784. <Form.Item label={t('pages.settings.subFormats.type')} name={[field.name, 'type']}>
  785. <Select
  786. options={['rand', 'base64', 'str', 'hex'].map((v) => ({
  787. value: v,
  788. label: v,
  789. }))}
  790. />
  791. </Form.Item>
  792. <Form.Item label={t('pages.settings.subFormats.packet')} name={[field.name, 'packet']}>
  793. <Input />
  794. </Form.Item>
  795. <Form.Item label={t('pages.settings.subFormats.delayMs')} name={[field.name, 'delay']}>
  796. <Input />
  797. </Form.Item>
  798. <Form.Item label={t('pages.settings.subFormats.applyTo')} name={[field.name, 'applyTo']}>
  799. <Select
  800. options={['ip', 'ipv4', 'ipv6'].map((v) => ({
  801. value: v,
  802. label: v,
  803. }))}
  804. />
  805. </Form.Item>
  806. </div>
  807. ))}
  808. </>
  809. )}
  810. </Form.List>
  811. <Form.List name={['settings', 'finalRules']}>
  812. {(fields, { add, remove }) => (
  813. <>
  814. <Form.Item label={t('pages.xray.outboundForm.finalRules')}>
  815. <Button
  816. size="small"
  817. type="primary"
  818. icon={<PlusOutlined />}
  819. onClick={() =>
  820. add({
  821. action: 'allow',
  822. network: '',
  823. port: '',
  824. ip: [],
  825. blockDelay: '',
  826. })
  827. }
  828. />
  829. <span className="ml-8" style={{ opacity: 0.6 }}>
  830. {t('pages.xray.outboundForm.overrideXrayPrivateIp')}
  831. </span>
  832. </Form.Item>
  833. {fields.map((field, index) => (
  834. <div key={field.key}>
  835. <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
  836. <div className="item-heading">
  837. <span>{t('pages.xray.outboundForm.ruleN', { n: index + 1 })}</span>
  838. <DeleteOutlined
  839. className="danger-icon"
  840. onClick={() => remove(field.name)}
  841. />
  842. </div>
  843. </Form.Item>
  844. <Form.Item label={t('pages.xray.outboundForm.action')} name={[field.name, 'action']}>
  845. <Select
  846. options={['allow', 'block'].map((v) => ({
  847. value: v,
  848. label: v,
  849. }))}
  850. />
  851. </Form.Item>
  852. <Form.Item label={t('pages.inbounds.network')} name={[field.name, 'network']}>
  853. <Select
  854. allowClear
  855. placeholder="(any)"
  856. options={['tcp', 'udp', 'tcp,udp'].map((v) => ({
  857. value: v,
  858. label: v,
  859. }))}
  860. />
  861. </Form.Item>
  862. <Form.Item label={t('pages.inbounds.port')} name={[field.name, 'port']}>
  863. <Input placeholder="e.g. 80,443 or 1000-2000" />
  864. </Form.Item>
  865. <Form.Item label="IP / CIDR / geoip" name={[field.name, 'ip']}>
  866. <Select
  867. mode="tags"
  868. tokenSeparators={[',', ' ']}
  869. placeholder="e.g. 10.0.0.0/8, geoip:private"
  870. />
  871. </Form.Item>
  872. <Form.Item shouldUpdate noStyle>
  873. {() => {
  874. const ruleAction = form.getFieldValue([
  875. 'settings',
  876. 'finalRules',
  877. field.name,
  878. 'action',
  879. ]);
  880. if (ruleAction !== 'block') return null;
  881. return (
  882. <Form.Item
  883. label={t('pages.xray.outboundForm.blockDelay')}
  884. name={[field.name, 'blockDelay']}
  885. >
  886. <Input placeholder="optional: 5000-10000" />
  887. </Form.Item>
  888. );
  889. }}
  890. </Form.Item>
  891. </div>
  892. ))}
  893. </>
  894. )}
  895. </Form.List>
  896. </>
  897. )}
  898. {protocol === 'vless' && (
  899. <Form.Item shouldUpdate noStyle>
  900. {() => {
  901. const reverseTag = form.getFieldValue(['settings', 'reverseTag']);
  902. if (!reverseTag) return null;
  903. const sniff = (form.getFieldValue(['settings', 'reverseSniffing']) ?? {}) as {
  904. enabled?: boolean;
  905. };
  906. return (
  907. <>
  908. <Form.Item
  909. label={t('pages.xray.outboundForm.reverseSniffing')}
  910. name={['settings', 'reverseSniffing', 'enabled']}
  911. valuePropName="checked"
  912. >
  913. <Switch />
  914. </Form.Item>
  915. {sniff.enabled && (
  916. <>
  917. <Form.Item
  918. wrapperCol={{ md: { span: 14, offset: 8 } }}
  919. name={['settings', 'reverseSniffing', 'destOverride']}
  920. >
  921. <Select
  922. mode="multiple"
  923. className="sniffing-options"
  924. options={Object.entries(SNIFFING_OPTION).map(([k, v]) => ({
  925. value: v,
  926. label: k,
  927. }))}
  928. />
  929. </Form.Item>
  930. <Form.Item
  931. label={t('pages.inbounds.sniffingMetadataOnly')}
  932. name={['settings', 'reverseSniffing', 'metadataOnly']}
  933. valuePropName="checked"
  934. >
  935. <Switch />
  936. </Form.Item>
  937. <Form.Item
  938. label={t('pages.inbounds.sniffingRouteOnly')}
  939. name={['settings', 'reverseSniffing', 'routeOnly']}
  940. valuePropName="checked"
  941. >
  942. <Switch />
  943. </Form.Item>
  944. <Form.Item
  945. label={t('pages.inbounds.sniffingIpsExcluded')}
  946. name={['settings', 'reverseSniffing', 'ipsExcluded']}
  947. >
  948. <Select
  949. mode="tags"
  950. tokenSeparators={[',']}
  951. placeholder="IP/CIDR/geoip:*"
  952. />
  953. </Form.Item>
  954. <Form.Item
  955. label={t('pages.inbounds.sniffingDomainsExcluded')}
  956. name={['settings', 'reverseSniffing', 'domainsExcluded']}
  957. >
  958. <Select
  959. mode="tags"
  960. tokenSeparators={[',']}
  961. placeholder="domain:*"
  962. />
  963. </Form.Item>
  964. </>
  965. )}
  966. </>
  967. );
  968. }}
  969. </Form.Item>
  970. )}
  971. {protocol === 'wireguard' && (
  972. <>
  973. <Form.Item label={t('pages.inbounds.address')} name={['settings', 'address']}>
  974. <Input placeholder="comma-separated, e.g. 10.0.0.1,fd00::1" />
  975. </Form.Item>
  976. <Form.Item label={t('pages.inbounds.privatekey')}>
  977. <Space.Compact block>
  978. <Form.Item name={['settings', 'secretKey']} noStyle>
  979. <Input style={{ width: 'calc(100% - 32px)' }} />
  980. </Form.Item>
  981. <Button
  982. icon={<ReloadOutlined />}
  983. onClick={() => {
  984. const pair = Wireguard.generateKeypair();
  985. form.setFieldValue(['settings', 'secretKey'], pair.privateKey);
  986. form.setFieldValue(['settings', 'pubKey'], pair.publicKey);
  987. }}
  988. />
  989. </Space.Compact>
  990. </Form.Item>
  991. <Form.Item label={t('pages.inbounds.publicKey')} name={['settings', 'pubKey']}>
  992. <Input disabled />
  993. </Form.Item>
  994. <Form.Item label={t('pages.xray.wireguard.domainStrategy')} name={['settings', 'domainStrategy']}>
  995. <Select
  996. options={[
  997. { value: '', label: `(${t('none')})` },
  998. ...WireguardDomainStrategy.map((s) => ({ value: s, label: s })),
  999. ]}
  1000. />
  1001. </Form.Item>
  1002. <Form.Item label="MTU" name={['settings', 'mtu']}>
  1003. <InputNumber min={0} />
  1004. </Form.Item>
  1005. <Form.Item label={t('pages.xray.outboundForm.workers')} name={['settings', 'workers']}>
  1006. <InputNumber min={0} />
  1007. </Form.Item>
  1008. <Form.Item
  1009. label={t('pages.inbounds.info.noKernelTun')}
  1010. name={['settings', 'noKernelTun']}
  1011. valuePropName="checked"
  1012. >
  1013. <Switch />
  1014. </Form.Item>
  1015. <Form.Item label={t('pages.xray.outboundForm.reserved')} name={['settings', 'reserved']}>
  1016. <Input placeholder="comma-separated bytes, e.g. 1,2,3" />
  1017. </Form.Item>
  1018. <Form.List name={['settings', 'peers']}>
  1019. {(fields, { add, remove }) => (
  1020. <>
  1021. <Form.Item label={t('pages.inbounds.form.peers')}>
  1022. <Button
  1023. size="small"
  1024. type="primary"
  1025. icon={<PlusOutlined />}
  1026. onClick={() =>
  1027. add({
  1028. publicKey: '',
  1029. psk: '',
  1030. allowedIPs: ['0.0.0.0/0', '::/0'],
  1031. endpoint: '',
  1032. keepAlive: 0,
  1033. })
  1034. }
  1035. />
  1036. </Form.Item>
  1037. {fields.map((field, index) => (
  1038. <div key={field.key}>
  1039. <Form.Item wrapperCol={{ md: { span: 14, offset: 8 } }}>
  1040. <div className="item-heading">
  1041. <span>{t('pages.inbounds.info.peerNumber', { n: index + 1 })}</span>
  1042. {fields.length > 1 && (
  1043. <DeleteOutlined
  1044. className="danger-icon"
  1045. onClick={() => remove(field.name)}
  1046. />
  1047. )}
  1048. </div>
  1049. </Form.Item>
  1050. <Form.Item label={t('pages.xray.wireguard.endpoint')} name={[field.name, 'endpoint']}>
  1051. <Input />
  1052. </Form.Item>
  1053. <Form.Item
  1054. label={t('pages.inbounds.publicKey')}
  1055. name={[field.name, 'publicKey']}
  1056. >
  1057. <Input />
  1058. </Form.Item>
  1059. <Form.Item label="PSK" name={[field.name, 'psk']}>
  1060. <Input />
  1061. </Form.Item>
  1062. <Form.Item label={t('pages.xray.wireguard.allowedIPs')}>
  1063. <Form.List name={[field.name, 'allowedIPs']}>
  1064. {(ipFields, { add: addIp, remove: removeIp }) => (
  1065. <>
  1066. {ipFields.map((ipField, ipIdx) => (
  1067. <Space.Compact
  1068. key={ipField.key}
  1069. block
  1070. style={{ marginBottom: 4 }}
  1071. >
  1072. <Form.Item noStyle name={ipField.name}>
  1073. <Input />
  1074. </Form.Item>
  1075. {ipFields.length > 1 && (
  1076. <InputAddon onClick={() => removeIp(ipIdx)}>
  1077. <MinusOutlined />
  1078. </InputAddon>
  1079. )}
  1080. </Space.Compact>
  1081. ))}
  1082. <Button
  1083. size="small"
  1084. icon={<PlusOutlined />}
  1085. onClick={() => addIp('')}
  1086. />
  1087. </>
  1088. )}
  1089. </Form.List>
  1090. </Form.Item>
  1091. <Form.Item label={t('pages.inbounds.info.keepAlive')} name={[field.name, 'keepAlive']}>
  1092. <InputNumber min={0} />
  1093. </Form.Item>
  1094. </div>
  1095. ))}
  1096. </>
  1097. )}
  1098. </Form.List>
  1099. </>
  1100. )}
  1101. {streamAllowed && network && (
  1102. <>
  1103. <Form.Item
  1104. label={t('transmission')}
  1105. name={['streamSettings', 'network']}
  1106. >
  1107. <Select
  1108. value={network}
  1109. onChange={onNetworkChange}
  1110. options={
  1111. protocol === 'hysteria'
  1112. ? [...NETWORK_OPTIONS, HYSTERIA_NETWORK_OPTION]
  1113. : NETWORK_OPTIONS
  1114. }
  1115. />
  1116. </Form.Item>
  1117. {network === 'tcp' && (
  1118. <Form.Item shouldUpdate noStyle>
  1119. {() => {
  1120. const type =
  1121. form.getFieldValue([
  1122. 'streamSettings',
  1123. 'tcpSettings',
  1124. 'header',
  1125. 'type',
  1126. ]) ?? 'none';
  1127. return (
  1128. <>
  1129. <Form.Item label={`HTTP ${t('camouflage')}`}>
  1130. <Switch
  1131. checked={type === 'http'}
  1132. onChange={(checked) =>
  1133. form.setFieldValue(
  1134. ['streamSettings', 'tcpSettings', 'header'],
  1135. checked
  1136. ? {
  1137. type: 'http',
  1138. request: {
  1139. version: '1.1',
  1140. method: 'GET',
  1141. path: ['/'],
  1142. headers: {},
  1143. },
  1144. response: {
  1145. version: '1.1',
  1146. status: '200',
  1147. reason: 'OK',
  1148. headers: {},
  1149. },
  1150. }
  1151. : { type: 'none' },
  1152. )
  1153. }
  1154. />
  1155. </Form.Item>
  1156. {type === 'http' && (
  1157. <>
  1158. <Form.Item
  1159. label={t('pages.inbounds.form.requestMethod')}
  1160. name={[
  1161. 'streamSettings', 'tcpSettings', 'header',
  1162. 'request', 'method',
  1163. ]}
  1164. >
  1165. <Input placeholder="GET" />
  1166. </Form.Item>
  1167. <Form.Item
  1168. label={t('pages.inbounds.form.requestVersion')}
  1169. name={[
  1170. 'streamSettings', 'tcpSettings', 'header',
  1171. 'request', 'version',
  1172. ]}
  1173. >
  1174. <Input placeholder="1.1" />
  1175. </Form.Item>
  1176. <Form.Item
  1177. label={t('pages.inbounds.form.requestHeaders')}
  1178. name={[
  1179. 'streamSettings', 'tcpSettings', 'header',
  1180. 'request', 'headers',
  1181. ]}
  1182. >
  1183. <HeaderMapEditor mode="v2" />
  1184. </Form.Item>
  1185. <Form.Item
  1186. label={t('pages.inbounds.form.responseVersion')}
  1187. name={[
  1188. 'streamSettings', 'tcpSettings', 'header',
  1189. 'response', 'version',
  1190. ]}
  1191. >
  1192. <Input placeholder="1.1" />
  1193. </Form.Item>
  1194. <Form.Item
  1195. label={t('pages.inbounds.form.responseStatus')}
  1196. name={[
  1197. 'streamSettings', 'tcpSettings', 'header',
  1198. 'response', 'status',
  1199. ]}
  1200. >
  1201. <Input placeholder="200" />
  1202. </Form.Item>
  1203. <Form.Item
  1204. label={t('pages.inbounds.form.responseReason')}
  1205. name={[
  1206. 'streamSettings', 'tcpSettings', 'header',
  1207. 'response', 'reason',
  1208. ]}
  1209. >
  1210. <Input placeholder="OK" />
  1211. </Form.Item>
  1212. <Form.Item
  1213. label={t('pages.inbounds.form.responseHeaders')}
  1214. name={[
  1215. 'streamSettings', 'tcpSettings', 'header',
  1216. 'response', 'headers',
  1217. ]}
  1218. >
  1219. <HeaderMapEditor mode="v2" />
  1220. </Form.Item>
  1221. </>
  1222. )}
  1223. </>
  1224. );
  1225. }}
  1226. </Form.Item>
  1227. )}
  1228. {network === 'kcp' && (
  1229. <>
  1230. <Form.Item label="MTU" name={['streamSettings', 'kcpSettings', 'mtu']}>
  1231. <InputNumber min={0} />
  1232. </Form.Item>
  1233. <Form.Item label={t('pages.inbounds.form.ttiMs')} name={['streamSettings', 'kcpSettings', 'tti']}>
  1234. <InputNumber min={0} />
  1235. </Form.Item>
  1236. <Form.Item
  1237. label={t('pages.inbounds.form.uplinkMbps')}
  1238. name={['streamSettings', 'kcpSettings', 'uplinkCapacity']}
  1239. >
  1240. <InputNumber min={0} />
  1241. </Form.Item>
  1242. <Form.Item
  1243. label={t('pages.inbounds.form.downlinkMbps')}
  1244. name={['streamSettings', 'kcpSettings', 'downlinkCapacity']}
  1245. >
  1246. <InputNumber min={0} />
  1247. </Form.Item>
  1248. <Form.Item
  1249. label={t('pages.inbounds.form.cwndMultiplier')}
  1250. name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
  1251. >
  1252. <InputNumber min={1} />
  1253. </Form.Item>
  1254. <Form.Item
  1255. label={t('pages.inbounds.form.maxSendingWindow')}
  1256. name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
  1257. >
  1258. <InputNumber min={0} />
  1259. </Form.Item>
  1260. </>
  1261. )}
  1262. {network === 'ws' && (
  1263. <>
  1264. <Form.Item label={t('host')} name={['streamSettings', 'wsSettings', 'host']}>
  1265. <Input />
  1266. </Form.Item>
  1267. <Form.Item label={t('path')} name={['streamSettings', 'wsSettings', 'path']}>
  1268. <Input />
  1269. </Form.Item>
  1270. <Form.Item
  1271. label={t('pages.inbounds.form.heartbeatPeriod')}
  1272. name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
  1273. >
  1274. <InputNumber min={0} />
  1275. </Form.Item>
  1276. <Form.Item
  1277. label={t('pages.inbounds.form.headers')}
  1278. name={['streamSettings', 'wsSettings', 'headers']}
  1279. >
  1280. <HeaderMapEditor mode="v1" />
  1281. </Form.Item>
  1282. </>
  1283. )}
  1284. {network === 'grpc' && (
  1285. <>
  1286. <Form.Item
  1287. label={t('pages.inbounds.form.serviceName')}
  1288. name={['streamSettings', 'grpcSettings', 'serviceName']}
  1289. >
  1290. <Input />
  1291. </Form.Item>
  1292. <Form.Item
  1293. label={t('pages.inbounds.form.authority')}
  1294. name={['streamSettings', 'grpcSettings', 'authority']}
  1295. >
  1296. <Input />
  1297. </Form.Item>
  1298. <Form.Item
  1299. label={t('pages.inbounds.form.multiMode')}
  1300. name={['streamSettings', 'grpcSettings', 'multiMode']}
  1301. valuePropName="checked"
  1302. >
  1303. <Switch />
  1304. </Form.Item>
  1305. </>
  1306. )}
  1307. {network === 'httpupgrade' && (
  1308. <>
  1309. <Form.Item
  1310. label={t('host')}
  1311. name={['streamSettings', 'httpupgradeSettings', 'host']}
  1312. >
  1313. <Input />
  1314. </Form.Item>
  1315. <Form.Item
  1316. label={t('path')}
  1317. name={['streamSettings', 'httpupgradeSettings', 'path']}
  1318. >
  1319. <Input />
  1320. </Form.Item>
  1321. <Form.Item
  1322. label={t('pages.inbounds.form.headers')}
  1323. name={['streamSettings', 'httpupgradeSettings', 'headers']}
  1324. >
  1325. <HeaderMapEditor mode="v1" />
  1326. </Form.Item>
  1327. </>
  1328. )}
  1329. {network === 'xhttp' && (
  1330. <>
  1331. <Form.Item
  1332. label={t('host')}
  1333. name={['streamSettings', 'xhttpSettings', 'host']}
  1334. >
  1335. <Input />
  1336. </Form.Item>
  1337. <Form.Item
  1338. label={t('path')}
  1339. name={['streamSettings', 'xhttpSettings', 'path']}
  1340. >
  1341. <Input />
  1342. </Form.Item>
  1343. <Form.Item
  1344. label={t('pages.inbounds.info.mode')}
  1345. name={['streamSettings', 'xhttpSettings', 'mode']}
  1346. >
  1347. <Select options={MODE_OPTIONS} />
  1348. </Form.Item>
  1349. <Form.Item
  1350. label={t('pages.inbounds.form.paddingBytes')}
  1351. name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
  1352. >
  1353. <Input />
  1354. </Form.Item>
  1355. <Form.Item
  1356. label={t('pages.inbounds.form.headers')}
  1357. name={['streamSettings', 'xhttpSettings', 'headers']}
  1358. >
  1359. <HeaderMapEditor mode="v1" />
  1360. </Form.Item>
  1361. {/* Padding obfs sub-section: gated by a Switch.
  1362. When on, four extra knobs (key/header/placement/
  1363. method) tune how Xray injects random padding to
  1364. disguise the post body shape. */}
  1365. <Form.Item
  1366. label={t('pages.inbounds.form.paddingObfsMode')}
  1367. name={['streamSettings', 'xhttpSettings', 'xPaddingObfsMode']}
  1368. valuePropName="checked"
  1369. >
  1370. <Switch />
  1371. </Form.Item>
  1372. <Form.Item shouldUpdate noStyle>
  1373. {() => {
  1374. const obfs = !!form.getFieldValue([
  1375. 'streamSettings', 'xhttpSettings', 'xPaddingObfsMode',
  1376. ]);
  1377. if (!obfs) return null;
  1378. return (
  1379. <>
  1380. <Form.Item
  1381. label={t('pages.inbounds.form.paddingKey')}
  1382. name={['streamSettings', 'xhttpSettings', 'xPaddingKey']}
  1383. >
  1384. <Input placeholder="x_padding" />
  1385. </Form.Item>
  1386. <Form.Item
  1387. label={t('pages.inbounds.form.paddingHeader')}
  1388. name={['streamSettings', 'xhttpSettings', 'xPaddingHeader']}
  1389. >
  1390. <Input placeholder="X-Padding" />
  1391. </Form.Item>
  1392. <Form.Item
  1393. label={t('pages.inbounds.form.paddingPlacement')}
  1394. name={['streamSettings', 'xhttpSettings', 'xPaddingPlacement']}
  1395. >
  1396. <Select
  1397. options={[
  1398. { value: '', label: 'Default (queryInHeader)' },
  1399. { value: 'queryInHeader', label: 'queryInHeader' },
  1400. { value: 'header', label: 'header' },
  1401. { value: 'cookie', label: 'cookie' },
  1402. { value: 'query', label: 'query' },
  1403. ]}
  1404. />
  1405. </Form.Item>
  1406. <Form.Item
  1407. label={t('pages.inbounds.form.paddingMethod')}
  1408. name={['streamSettings', 'xhttpSettings', 'xPaddingMethod']}
  1409. >
  1410. <Select
  1411. options={[
  1412. { value: '', label: 'Default (repeat-x)' },
  1413. { value: 'repeat-x', label: 'repeat-x' },
  1414. { value: 'tokenish', label: 'tokenish' },
  1415. ]}
  1416. />
  1417. </Form.Item>
  1418. </>
  1419. );
  1420. }}
  1421. </Form.Item>
  1422. <Form.Item
  1423. noStyle
  1424. shouldUpdate={(prev, curr) =>
  1425. prev?.streamSettings?.xhttpSettings?.mode !==
  1426. curr?.streamSettings?.xhttpSettings?.mode
  1427. }
  1428. >
  1429. {() => {
  1430. const mode = form.getFieldValue([
  1431. 'streamSettings', 'xhttpSettings', 'mode',
  1432. ]);
  1433. return (
  1434. <Form.Item
  1435. label={t('pages.inbounds.form.uplinkHttpMethod')}
  1436. name={['streamSettings', 'xhttpSettings', 'uplinkHTTPMethod']}
  1437. >
  1438. <Select
  1439. placeholder="Default (POST)"
  1440. options={[
  1441. { value: '', label: 'Default (POST)' },
  1442. { value: 'POST', label: 'POST' },
  1443. { value: 'PUT', label: 'PUT' },
  1444. { value: 'GET', label: 'GET (packet-up only)', disabled: mode !== 'packet-up' },
  1445. ]}
  1446. />
  1447. </Form.Item>
  1448. );
  1449. }}
  1450. </Form.Item>
  1451. {/* Session + sequence + uplinkData placements:
  1452. three orthogonal slots Xray uses to thread
  1453. request metadata through the transport
  1454. (path / header / cookie / query). Key field
  1455. only matters when placement is not 'path'. */}
  1456. <Form.Item
  1457. label={t('pages.inbounds.form.sessionPlacement')}
  1458. name={['streamSettings', 'xhttpSettings', 'sessionPlacement']}
  1459. >
  1460. <Select
  1461. placeholder="Default (path)"
  1462. options={[
  1463. { value: '', label: 'Default (path)' },
  1464. { value: 'path', label: 'path' },
  1465. { value: 'header', label: 'header' },
  1466. { value: 'cookie', label: 'cookie' },
  1467. { value: 'query', label: 'query' },
  1468. ]}
  1469. />
  1470. </Form.Item>
  1471. <Form.Item shouldUpdate noStyle>
  1472. {() => {
  1473. const placement = form.getFieldValue([
  1474. 'streamSettings', 'xhttpSettings', 'sessionPlacement',
  1475. ]);
  1476. if (!placement || placement === 'path') return null;
  1477. return (
  1478. <Form.Item
  1479. label={t('pages.inbounds.form.sessionKey')}
  1480. name={['streamSettings', 'xhttpSettings', 'sessionKey']}
  1481. >
  1482. <Input placeholder="x_session" />
  1483. </Form.Item>
  1484. );
  1485. }}
  1486. </Form.Item>
  1487. <Form.Item
  1488. label={t('pages.inbounds.form.sequencePlacement')}
  1489. name={['streamSettings', 'xhttpSettings', 'seqPlacement']}
  1490. >
  1491. <Select
  1492. placeholder="Default (path)"
  1493. options={[
  1494. { value: '', label: 'Default (path)' },
  1495. { value: 'path', label: 'path' },
  1496. { value: 'header', label: 'header' },
  1497. { value: 'cookie', label: 'cookie' },
  1498. { value: 'query', label: 'query' },
  1499. ]}
  1500. />
  1501. </Form.Item>
  1502. <Form.Item shouldUpdate noStyle>
  1503. {() => {
  1504. const placement = form.getFieldValue([
  1505. 'streamSettings', 'xhttpSettings', 'seqPlacement',
  1506. ]);
  1507. if (!placement || placement === 'path') return null;
  1508. return (
  1509. <Form.Item
  1510. label={t('pages.inbounds.form.sequenceKey')}
  1511. name={['streamSettings', 'xhttpSettings', 'seqKey']}
  1512. >
  1513. <Input placeholder="x_seq" />
  1514. </Form.Item>
  1515. );
  1516. }}
  1517. </Form.Item>
  1518. {/* Mode-conditional sub-sections. */}
  1519. <Form.Item shouldUpdate noStyle>
  1520. {() => {
  1521. const mode = form.getFieldValue([
  1522. 'streamSettings', 'xhttpSettings', 'mode',
  1523. ]);
  1524. if (mode !== 'packet-up') return null;
  1525. return (
  1526. <>
  1527. <Form.Item
  1528. label={t('pages.xray.outboundForm.minUploadInterval')}
  1529. name={['streamSettings', 'xhttpSettings', 'scMinPostsIntervalMs']}
  1530. >
  1531. <Input placeholder="30" />
  1532. </Form.Item>
  1533. <Form.Item
  1534. label={t('pages.xray.outboundForm.maxUploadSizeBytes')}
  1535. name={['streamSettings', 'xhttpSettings', 'scMaxEachPostBytes']}
  1536. >
  1537. <Input placeholder="1000000" />
  1538. </Form.Item>
  1539. <Form.Item
  1540. label={t('pages.inbounds.form.uplinkDataPlacement')}
  1541. name={['streamSettings', 'xhttpSettings', 'uplinkDataPlacement']}
  1542. >
  1543. <Select
  1544. options={[
  1545. { value: '', label: 'Default (body)' },
  1546. { value: 'body', label: 'body' },
  1547. { value: 'header', label: 'header' },
  1548. { value: 'cookie', label: 'cookie' },
  1549. { value: 'query', label: 'query' },
  1550. ]}
  1551. />
  1552. </Form.Item>
  1553. <Form.Item shouldUpdate noStyle>
  1554. {() => {
  1555. const place = form.getFieldValue([
  1556. 'streamSettings', 'xhttpSettings', 'uplinkDataPlacement',
  1557. ]);
  1558. if (!place || place === 'body') return null;
  1559. return (
  1560. <>
  1561. <Form.Item
  1562. label={t('pages.inbounds.form.uplinkDataKey')}
  1563. name={['streamSettings', 'xhttpSettings', 'uplinkDataKey']}
  1564. >
  1565. <Input placeholder="x_data" />
  1566. </Form.Item>
  1567. <Form.Item
  1568. label={t('pages.xray.outboundForm.uplinkChunkSize')}
  1569. name={['streamSettings', 'xhttpSettings', 'uplinkChunkSize']}
  1570. >
  1571. <InputNumber
  1572. min={0}
  1573. placeholder="0 (unlimited)"
  1574. style={{ width: '100%' }}
  1575. />
  1576. </Form.Item>
  1577. </>
  1578. );
  1579. }}
  1580. </Form.Item>
  1581. </>
  1582. );
  1583. }}
  1584. </Form.Item>
  1585. <Form.Item shouldUpdate noStyle>
  1586. {() => {
  1587. const mode = form.getFieldValue([
  1588. 'streamSettings', 'xhttpSettings', 'mode',
  1589. ]);
  1590. if (mode !== 'stream-up' && mode !== 'stream-one') return null;
  1591. return (
  1592. <Form.Item
  1593. label={t('pages.xray.outboundForm.noGrpcHeader')}
  1594. name={['streamSettings', 'xhttpSettings', 'noGRPCHeader']}
  1595. valuePropName="checked"
  1596. >
  1597. <Switch />
  1598. </Form.Item>
  1599. );
  1600. }}
  1601. </Form.Item>
  1602. {/* XMUX is the connection-multiplexing layer
  1603. xHTTP uses to fan out parallel requests over
  1604. a small pool of upstream connections. UI-only
  1605. toggle (enableXmux) hides the 6 nested knobs
  1606. when off. */}
  1607. <Form.Item
  1608. label="XMUX"
  1609. name={['streamSettings', 'xhttpSettings', 'enableXmux']}
  1610. valuePropName="checked"
  1611. >
  1612. <Switch onChange={onXmuxToggle} />
  1613. </Form.Item>
  1614. <Form.Item shouldUpdate noStyle>
  1615. {() => {
  1616. if (!form.getFieldValue([
  1617. 'streamSettings', 'xhttpSettings', 'enableXmux',
  1618. ])) return null;
  1619. return (
  1620. <>
  1621. <Form.Item
  1622. label={t('pages.xray.outboundForm.maxConcurrency')}
  1623. name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConcurrency']}
  1624. >
  1625. <Input placeholder="16-32" />
  1626. </Form.Item>
  1627. <Form.Item
  1628. label={t('pages.xray.outboundForm.maxConnections')}
  1629. name={['streamSettings', 'xhttpSettings', 'xmux', 'maxConnections']}
  1630. >
  1631. <Input placeholder="0" />
  1632. </Form.Item>
  1633. <Form.Item
  1634. label={t('pages.xray.outboundForm.maxReuseTimes')}
  1635. name={['streamSettings', 'xhttpSettings', 'xmux', 'cMaxReuseTimes']}
  1636. >
  1637. <Input />
  1638. </Form.Item>
  1639. <Form.Item
  1640. label={t('pages.xray.outboundForm.maxRequestTimes')}
  1641. name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxRequestTimes']}
  1642. >
  1643. <Input placeholder="600-900" />
  1644. </Form.Item>
  1645. <Form.Item
  1646. label={t('pages.xray.outboundForm.maxReusableSecs')}
  1647. name={['streamSettings', 'xhttpSettings', 'xmux', 'hMaxReusableSecs']}
  1648. >
  1649. <Input placeholder="1800-3000" />
  1650. </Form.Item>
  1651. <Form.Item
  1652. label={t('pages.xray.outboundForm.keepAlivePeriod')}
  1653. name={['streamSettings', 'xhttpSettings', 'xmux', 'hKeepAlivePeriod']}
  1654. >
  1655. <InputNumber min={0} style={{ width: '100%' }} />
  1656. </Form.Item>
  1657. </>
  1658. );
  1659. }}
  1660. </Form.Item>
  1661. </>
  1662. )}
  1663. {network === 'hysteria' && (
  1664. <>
  1665. <Form.Item
  1666. label={t('pages.xray.outboundForm.authPassword')}
  1667. name={['streamSettings', 'hysteriaSettings', 'auth']}
  1668. >
  1669. <Input />
  1670. </Form.Item>
  1671. <Form.Item
  1672. label={t('pages.inbounds.form.udpIdleTimeout')}
  1673. name={['streamSettings', 'hysteriaSettings', 'udpIdleTimeout']}
  1674. >
  1675. <InputNumber min={1} style={{ width: '100%' }} />
  1676. </Form.Item>
  1677. </>
  1678. )}
  1679. </>
  1680. )}
  1681. {tlsFlowAllowed && (
  1682. <Form.Item label={t('pages.clients.flow')} name={['settings', 'flow']}>
  1683. <Select
  1684. allowClear
  1685. placeholder={t('none')}
  1686. options={FLOW_OPTIONS}
  1687. />
  1688. </Form.Item>
  1689. )}
  1690. {/* Vision seed knobs only meaningful for the exact
  1691. xtls-rprx-vision flow, on TCP+(tls|reality). The
  1692. legacy class gated this on `canEnableVisionSeed()`
  1693. — same condition encoded inline here. */}
  1694. <Form.Item shouldUpdate noStyle>
  1695. {() => {
  1696. const flow =
  1697. (form.getFieldValue(['settings', 'flow']) ?? '') as string;
  1698. if (!(tlsFlowAllowed && flow === 'xtls-rprx-vision')) return null;
  1699. return (
  1700. <>
  1701. <Form.Item label={t('pages.xray.outboundForm.visionTestpre')} name={['settings', 'testpre']}>
  1702. <InputNumber min={0} style={{ width: '100%' }} />
  1703. </Form.Item>
  1704. <Form.Item
  1705. label={t('pages.inbounds.form.visionTestseed')}
  1706. name={['settings', 'testseed']}
  1707. normalize={(v: unknown) =>
  1708. Array.isArray(v)
  1709. ? v
  1710. .map((x) => Number(x))
  1711. .filter((n) => Number.isInteger(n) && n > 0)
  1712. : []
  1713. }
  1714. >
  1715. <Select
  1716. mode="tags"
  1717. tokenSeparators={[',', ' ']}
  1718. placeholder="four positive integers"
  1719. />
  1720. </Form.Item>
  1721. </>
  1722. );
  1723. }}
  1724. </Form.Item>
  1725. {streamAllowed && network && (
  1726. <Form.Item label={t('security')}>
  1727. <Radio.Group
  1728. value={security}
  1729. buttonStyle="solid"
  1730. onChange={(e) => onSecurityChange(e.target.value as string)}
  1731. >
  1732. <Radio.Button value="none">{t('none')}</Radio.Button>
  1733. {tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
  1734. {realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
  1735. </Radio.Group>
  1736. </Form.Item>
  1737. )}
  1738. {security === 'tls' && tlsAllowed && (
  1739. <>
  1740. <Form.Item
  1741. label="SNI"
  1742. name={['streamSettings', 'tlsSettings', 'serverName']}
  1743. >
  1744. <Input placeholder={t('pages.xray.outboundForm.serverNamePlaceholder')} />
  1745. </Form.Item>
  1746. <Form.Item
  1747. label="uTLS"
  1748. name={['streamSettings', 'tlsSettings', 'fingerprint']}
  1749. >
  1750. <Select
  1751. allowClear
  1752. placeholder={t('none')}
  1753. options={UTLS_OPTIONS}
  1754. />
  1755. </Form.Item>
  1756. <Form.Item
  1757. label="ALPN"
  1758. name={['streamSettings', 'tlsSettings', 'alpn']}
  1759. >
  1760. <Select mode="multiple" options={ALPN_OPTIONS} />
  1761. </Form.Item>
  1762. <Form.Item
  1763. label="ECH"
  1764. name={['streamSettings', 'tlsSettings', 'echConfigList']}
  1765. >
  1766. <Input />
  1767. </Form.Item>
  1768. <Form.Item
  1769. label={t('pages.xray.outboundForm.verifyPeerName')}
  1770. name={['streamSettings', 'tlsSettings', 'verifyPeerCertByName']}
  1771. >
  1772. <Input placeholder="cloudflare-dns.com" />
  1773. </Form.Item>
  1774. <Form.Item
  1775. label={t('pages.xray.outboundForm.pinnedSha256')}
  1776. name={['streamSettings', 'tlsSettings', 'pinnedPeerCertSha256']}
  1777. >
  1778. <Input placeholder="base64 SHA256" />
  1779. </Form.Item>
  1780. </>
  1781. )}
  1782. {security === 'reality' && realityAllowed && (
  1783. <>
  1784. <Form.Item
  1785. label="SNI"
  1786. name={['streamSettings', 'realitySettings', 'serverName']}
  1787. >
  1788. <Input />
  1789. </Form.Item>
  1790. <Form.Item
  1791. label="uTLS"
  1792. name={['streamSettings', 'realitySettings', 'fingerprint']}
  1793. >
  1794. <Select options={UTLS_OPTIONS} />
  1795. </Form.Item>
  1796. <Form.Item
  1797. label={t('pages.xray.outboundForm.shortId')}
  1798. name={['streamSettings', 'realitySettings', 'shortId']}
  1799. >
  1800. <Input />
  1801. </Form.Item>
  1802. <Form.Item
  1803. label={t('pages.inbounds.form.spiderX')}
  1804. name={['streamSettings', 'realitySettings', 'spiderX']}
  1805. >
  1806. <Input />
  1807. </Form.Item>
  1808. <Form.Item
  1809. label={t('pages.inbounds.publicKey')}
  1810. name={['streamSettings', 'realitySettings', 'publicKey']}
  1811. >
  1812. <Input.TextArea autoSize={{ minRows: 2 }} />
  1813. </Form.Item>
  1814. <Form.Item
  1815. label={t('pages.inbounds.form.mldsa65Verify')}
  1816. name={['streamSettings', 'realitySettings', 'mldsa65Verify']}
  1817. >
  1818. <Input.TextArea autoSize={{ minRows: 2 }} />
  1819. </Form.Item>
  1820. </>
  1821. )}
  1822. {((streamAllowed && network) || !streamAllowed) && (
  1823. <Form.Item shouldUpdate noStyle>
  1824. {() => {
  1825. const hasSockopt = !!form.getFieldValue([
  1826. 'streamSettings',
  1827. 'sockopt',
  1828. ]);
  1829. return (
  1830. <>
  1831. <Form.Item label={t('pages.xray.outboundForm.sockopts')}>
  1832. <Switch
  1833. checked={hasSockopt}
  1834. onChange={(checked) => {
  1835. form.setFieldValue(
  1836. ['streamSettings', 'sockopt'],
  1837. checked ? SockoptStreamSettingsSchema.parse({}) : undefined,
  1838. );
  1839. }}
  1840. />
  1841. </Form.Item>
  1842. {hasSockopt && (
  1843. <>
  1844. <Form.Item
  1845. label={t('pages.inbounds.form.dialerProxy')}
  1846. name={['streamSettings', 'sockopt', 'dialerProxy']}
  1847. >
  1848. <Input />
  1849. </Form.Item>
  1850. <Form.Item
  1851. label={t('pages.xray.wireguard.domainStrategy')}
  1852. name={['streamSettings', 'sockopt', 'domainStrategy']}
  1853. >
  1854. <Select
  1855. options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
  1856. value: v,
  1857. label: v,
  1858. }))}
  1859. />
  1860. </Form.Item>
  1861. <Form.Item
  1862. label={t('pages.inbounds.form.addressPortStrategy')}
  1863. name={['streamSettings', 'sockopt', 'addressPortStrategy']}
  1864. >
  1865. <Select options={ADDRESS_PORT_STRATEGY_OPTIONS} />
  1866. </Form.Item>
  1867. <Form.Item
  1868. label={t('pages.xray.outboundForm.keepAliveInterval')}
  1869. name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
  1870. >
  1871. <InputNumber min={0} />
  1872. </Form.Item>
  1873. <Form.Item
  1874. label={t('pages.inbounds.form.tcpFastOpen')}
  1875. name={['streamSettings', 'sockopt', 'tcpFastOpen']}
  1876. valuePropName="checked"
  1877. >
  1878. <Switch />
  1879. </Form.Item>
  1880. <Form.Item
  1881. label={t('pages.inbounds.form.multipathTcp')}
  1882. name={['streamSettings', 'sockopt', 'tcpMptcp']}
  1883. valuePropName="checked"
  1884. >
  1885. <Switch />
  1886. </Form.Item>
  1887. <Form.Item
  1888. label={t('pages.inbounds.form.penetrate')}
  1889. name={['streamSettings', 'sockopt', 'penetrate']}
  1890. valuePropName="checked"
  1891. >
  1892. <Switch />
  1893. </Form.Item>
  1894. <Form.Item
  1895. label={t('pages.xray.outboundForm.markFwmark')}
  1896. name={['streamSettings', 'sockopt', 'mark']}
  1897. >
  1898. <InputNumber min={0} />
  1899. </Form.Item>
  1900. <Form.Item
  1901. label={t('pages.xray.outboundForm.interface')}
  1902. name={['streamSettings', 'sockopt', 'interfaceName']}
  1903. >
  1904. <Input />
  1905. </Form.Item>
  1906. <Form.Item
  1907. label="TProxy"
  1908. name={['streamSettings', 'sockopt', 'tproxy']}
  1909. >
  1910. <Select
  1911. options={[
  1912. { value: 'off', label: 'off' },
  1913. { value: 'redirect', label: 'redirect' },
  1914. { value: 'tproxy', label: 'tproxy' },
  1915. ]}
  1916. />
  1917. </Form.Item>
  1918. <Form.Item
  1919. label={t('pages.inbounds.form.tcpCongestion')}
  1920. name={['streamSettings', 'sockopt', 'tcpcongestion']}
  1921. >
  1922. <Select
  1923. options={Object.values(TCP_CONGESTION_OPTION).map((v) => ({
  1924. value: v,
  1925. label: v,
  1926. }))}
  1927. />
  1928. </Form.Item>
  1929. <Form.Item
  1930. label={t('pages.xray.outboundForm.ipv6Only')}
  1931. name={['streamSettings', 'sockopt', 'V6Only']}
  1932. valuePropName="checked"
  1933. >
  1934. <Switch />
  1935. </Form.Item>
  1936. <Form.Item
  1937. label={t('pages.xray.outboundForm.acceptProxyProtocol')}
  1938. name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
  1939. valuePropName="checked"
  1940. >
  1941. <Switch />
  1942. </Form.Item>
  1943. <Form.Item
  1944. label={t('pages.xray.outboundForm.tcpUserTimeoutMs')}
  1945. name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
  1946. >
  1947. <InputNumber min={0} style={{ width: '100%' }} />
  1948. </Form.Item>
  1949. <Form.Item
  1950. label={t('pages.xray.outboundForm.tcpKeepAliveIdleS')}
  1951. name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
  1952. >
  1953. <InputNumber min={0} style={{ width: '100%' }} />
  1954. </Form.Item>
  1955. <Form.Item
  1956. label={t('pages.inbounds.form.tcpMaxSeg')}
  1957. name={['streamSettings', 'sockopt', 'tcpMaxSeg']}
  1958. >
  1959. <InputNumber min={0} style={{ width: '100%' }} />
  1960. </Form.Item>
  1961. <Form.Item
  1962. label={t('pages.inbounds.form.tcpWindowClamp')}
  1963. name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
  1964. >
  1965. <InputNumber min={0} style={{ width: '100%' }} />
  1966. </Form.Item>
  1967. <Form.Item
  1968. label={t('pages.inbounds.form.trustedXForwardedFor')}
  1969. name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
  1970. >
  1971. <Select
  1972. mode="tags"
  1973. tokenSeparators={[',', ' ']}
  1974. placeholder="trusted-proxy.example,10.0.0.0/8"
  1975. />
  1976. </Form.Item>
  1977. <Form.Item shouldUpdate noStyle>
  1978. {() => {
  1979. const he = form.getFieldValue([
  1980. 'streamSettings', 'sockopt', 'happyEyeballs',
  1981. ]);
  1982. const hasHe = he != null;
  1983. return (
  1984. <>
  1985. <Form.Item label="Happy Eyeballs">
  1986. <Switch
  1987. checked={hasHe}
  1988. onChange={(v) => {
  1989. form.setFieldValue(
  1990. ['streamSettings', 'sockopt', 'happyEyeballs'],
  1991. v ? HappyEyeballsSchema.parse({}) : undefined,
  1992. );
  1993. }}
  1994. />
  1995. </Form.Item>
  1996. {hasHe && (
  1997. <>
  1998. <Form.Item
  1999. label={t('pages.inbounds.form.tryDelayMs')}
  2000. name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
  2001. >
  2002. <InputNumber min={0} style={{ width: '100%' }} placeholder="0 (disabled) — 250 recommended" />
  2003. </Form.Item>
  2004. <Form.Item
  2005. label={t('pages.inbounds.form.prioritizeIPv6')}
  2006. name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
  2007. valuePropName="checked"
  2008. >
  2009. <Switch />
  2010. </Form.Item>
  2011. <Form.Item
  2012. label={t('pages.inbounds.form.interleave')}
  2013. name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
  2014. >
  2015. <InputNumber min={1} style={{ width: '100%' }} />
  2016. </Form.Item>
  2017. <Form.Item
  2018. label={t('pages.inbounds.form.maxConcurrentTry')}
  2019. name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
  2020. >
  2021. <InputNumber min={0} style={{ width: '100%' }} />
  2022. </Form.Item>
  2023. </>
  2024. )}
  2025. </>
  2026. );
  2027. }}
  2028. </Form.Item>
  2029. <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
  2030. {(fields, { add, remove }) => (
  2031. <>
  2032. <Form.Item label={t('pages.inbounds.form.customSockopt')}>
  2033. <Button
  2034. type="dashed"
  2035. size="small"
  2036. onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
  2037. >
  2038. + {t('pages.inbounds.form.addCustomOption')}
  2039. </Button>
  2040. </Form.Item>
  2041. {fields.map((field) => (
  2042. <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
  2043. <Form.Item name={[field.name, 'system']} noStyle>
  2044. <Select
  2045. placeholder="all"
  2046. allowClear
  2047. style={{ width: 100 }}
  2048. options={[
  2049. { value: 'linux', label: 'linux' },
  2050. { value: 'windows', label: 'windows' },
  2051. { value: 'darwin', label: 'darwin' },
  2052. ]}
  2053. />
  2054. </Form.Item>
  2055. <Form.Item name={[field.name, 'type']} noStyle>
  2056. <Select
  2057. style={{ width: 80 }}
  2058. options={[
  2059. { value: 'int', label: 'int' },
  2060. { value: 'str', label: 'str' },
  2061. ]}
  2062. />
  2063. </Form.Item>
  2064. <Form.Item name={[field.name, 'level']} noStyle>
  2065. <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
  2066. </Form.Item>
  2067. <Form.Item name={[field.name, 'opt']} noStyle>
  2068. <Input placeholder="opt (decimal)" style={{ width: 120 }} />
  2069. </Form.Item>
  2070. <Form.Item name={[field.name, 'value']} noStyle>
  2071. <Input placeholder="value" style={{ flex: 1 }} />
  2072. </Form.Item>
  2073. <Button danger onClick={() => remove(field.name)}>−</Button>
  2074. </Space.Compact>
  2075. ))}
  2076. </>
  2077. )}
  2078. </Form.List>
  2079. </>
  2080. )}
  2081. </>
  2082. );
  2083. }}
  2084. </Form.Item>
  2085. )}
  2086. <FinalMaskForm
  2087. name={['streamSettings', 'finalmask']}
  2088. network={network}
  2089. protocol={protocol}
  2090. form={form}
  2091. />
  2092. {(() => {
  2093. const flow = (form.getFieldValue(['settings', 'flow']) ?? '') as string;
  2094. if (!isMuxAllowed(protocol, flow, network)) return null;
  2095. return (
  2096. <Form.Item shouldUpdate noStyle>
  2097. {() => {
  2098. const muxEnabled = !!form.getFieldValue(['mux', 'enabled']);
  2099. return (
  2100. <>
  2101. <Form.Item
  2102. label={t('pages.settings.mux')}
  2103. name={['mux', 'enabled']}
  2104. valuePropName="checked"
  2105. >
  2106. <Switch />
  2107. </Form.Item>
  2108. {muxEnabled && (
  2109. <>
  2110. <Form.Item
  2111. label={t('pages.settings.subFormats.concurrency')}
  2112. name={['mux', 'concurrency']}
  2113. >
  2114. <InputNumber min={-1} max={1024} />
  2115. </Form.Item>
  2116. <Form.Item
  2117. label={t('pages.settings.subFormats.xudpConcurrency')}
  2118. name={['mux', 'xudpConcurrency']}
  2119. >
  2120. <InputNumber min={-1} max={1024} />
  2121. </Form.Item>
  2122. <Form.Item
  2123. label={t('pages.settings.subFormats.xudpUdp443')}
  2124. name={['mux', 'xudpProxyUDP443']}
  2125. >
  2126. <Select
  2127. options={['reject', 'allow', 'skip'].map((v) => ({
  2128. value: v,
  2129. label: v,
  2130. }))}
  2131. />
  2132. </Form.Item>
  2133. </>
  2134. )}
  2135. </>
  2136. );
  2137. }}
  2138. </Form.Item>
  2139. );
  2140. })()}
  2141. </>
  2142. ),
  2143. },
  2144. {
  2145. key: '2',
  2146. label: 'JSON',
  2147. children: (
  2148. <Space orientation="vertical" size={10} style={{ width: '100%', marginTop: 10 }}>
  2149. <Input.Search
  2150. value={linkInput}
  2151. placeholder="vmess:// vless:// trojan:// ss:// hysteria2://"
  2152. enterButton="Import"
  2153. onChange={(e) => setLinkInput(e.target.value)}
  2154. onSearch={importLink}
  2155. />
  2156. <JsonEditor
  2157. value={jsonText}
  2158. onChange={(next) => {
  2159. setJsonText(next);
  2160. setJsonDirty(true);
  2161. }}
  2162. minHeight="360px"
  2163. maxHeight="600px"
  2164. />
  2165. </Space>
  2166. ),
  2167. },
  2168. ]}
  2169. />
  2170. </Form>
  2171. </Modal>
  2172. </>
  2173. );
  2174. }