sockopt.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import { useTranslation } from 'react-i18next';
  2. import { Alert, Button, Form, Input, InputNumber, Segmented, Select, Space, Switch } from 'antd';
  3. import {
  4. Address_Port_Strategy,
  5. DOMAIN_STRATEGY_OPTION,
  6. TCP_CONGESTION_OPTION,
  7. } from '@/schemas/primitives';
  8. import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
  9. // Transport key that carries its own acceptProxyProtocol field (mirrored
  10. // alongside the sockopt-level one so the PROXY preset never silently no-ops).
  11. const TRANSPORT_PROXY_FIELD: Record<string, string> = {
  12. tcp: 'tcpSettings',
  13. ws: 'wsSettings',
  14. httpupgrade: 'httpupgradeSettings',
  15. };
  16. // Transports on which xray-core honors sockopt.trustedXForwardedFor.
  17. const TRUSTED_HEADER_NETWORKS = ['ws', 'httpupgrade', 'xhttp'];
  18. type RealClientIpPreset = 'off' | 'cloudflare' | 'proxy';
  19. export default function SockoptForm({
  20. toggleSockopt,
  21. network,
  22. }: {
  23. toggleSockopt: (on: boolean) => void;
  24. network: string;
  25. }) {
  26. const { t } = useTranslation();
  27. // Presets write the same sockopt fields the user could set by hand below,
  28. // picking the mechanism xray-core actually honors for the chosen transport:
  29. // CF-Connecting-IP via trustedXForwardedFor (ws/httpupgrade/xhttp) or the
  30. // PROXY-protocol header via acceptProxyProtocol (every transport but mKCP).
  31. const applyRealClientIpPreset = (
  32. preset: RealClientIpPreset,
  33. getFieldValue: (name: (string | number)[]) => unknown,
  34. setFieldValue: (name: (string | number)[], value: unknown) => void,
  35. ) => {
  36. const sockopt = getFieldValue(['streamSettings', 'sockopt']);
  37. const sockoptOn =
  38. !!sockopt && typeof sockopt === 'object' && Object.keys(sockopt as object).length > 0;
  39. if (preset !== 'off' && !sockoptOn) {
  40. toggleSockopt(true);
  41. }
  42. const transportField = TRANSPORT_PROXY_FIELD[network];
  43. if (preset === 'off') {
  44. setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], []);
  45. setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], false);
  46. if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], false);
  47. return;
  48. }
  49. if (preset === 'cloudflare') {
  50. const current = getFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor']);
  51. const list = Array.isArray(current) ? [...(current as string[])] : [];
  52. if (!list.includes('CF-Connecting-IP')) list.push('CF-Connecting-IP');
  53. setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], list);
  54. setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], false);
  55. if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], false);
  56. return;
  57. }
  58. // proxy — clear trustedXForwardedFor so a lingering header can't override the
  59. // PROXY-recovered IP (xray reads the header last on ws/httpupgrade/xhttp).
  60. setFieldValue(['streamSettings', 'sockopt', 'trustedXForwardedFor'], []);
  61. setFieldValue(['streamSettings', 'sockopt', 'acceptProxyProtocol'], true);
  62. if (transportField) setFieldValue(['streamSettings', transportField, 'acceptProxyProtocol'], true);
  63. };
  64. return (
  65. <Form.Item
  66. noStyle
  67. shouldUpdate={(prev, curr) => {
  68. const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
  69. const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
  70. return !!a !== !!b;
  71. }}
  72. >
  73. {({ getFieldValue }) => {
  74. const sock = getFieldValue(['streamSettings', 'sockopt']);
  75. const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
  76. return (
  77. <>
  78. <Form.Item label="Sockopt">
  79. <Switch checked={on} onChange={toggleSockopt} />
  80. </Form.Item>
  81. {on && (
  82. <>
  83. <Form.Item
  84. noStyle
  85. shouldUpdate={(prev, curr) => {
  86. type ProxyWatch = {
  87. streamSettings?: {
  88. sockopt?: { trustedXForwardedFor?: unknown; acceptProxyProtocol?: unknown };
  89. tcpSettings?: { acceptProxyProtocol?: unknown };
  90. wsSettings?: { acceptProxyProtocol?: unknown };
  91. httpupgradeSettings?: { acceptProxyProtocol?: unknown };
  92. };
  93. };
  94. const pick = (v: ProxyWatch) => {
  95. const s = v.streamSettings;
  96. return JSON.stringify([
  97. s?.sockopt?.trustedXForwardedFor,
  98. s?.sockopt?.acceptProxyProtocol,
  99. s?.tcpSettings?.acceptProxyProtocol,
  100. s?.wsSettings?.acceptProxyProtocol,
  101. s?.httpupgradeSettings?.acceptProxyProtocol,
  102. ]);
  103. };
  104. return pick(prev as ProxyWatch) !== pick(curr as ProxyWatch);
  105. }}
  106. >
  107. {({ getFieldValue, setFieldValue }) => {
  108. const sockopt = (getFieldValue(['streamSettings', 'sockopt']) ?? {}) as Record<
  109. string,
  110. unknown
  111. >;
  112. const transportField = TRANSPORT_PROXY_FIELD[network];
  113. const transportPP = transportField
  114. ? getFieldValue(['streamSettings', transportField, 'acceptProxyProtocol']) === true
  115. : false;
  116. const proxyOn = sockopt.acceptProxyProtocol === true || transportPP;
  117. const trusted = Array.isArray(sockopt.trustedXForwardedFor)
  118. ? (sockopt.trustedXForwardedFor as string[])
  119. : [];
  120. const value: RealClientIpPreset = proxyOn
  121. ? 'proxy'
  122. : trusted.length > 0
  123. ? 'cloudflare'
  124. : 'off';
  125. const trustedMismatch =
  126. trusted.length > 0 && !TRUSTED_HEADER_NETWORKS.includes(network);
  127. const proxyMismatch = proxyOn && network === 'kcp';
  128. return (
  129. <>
  130. <Form.Item
  131. label={t('pages.inbounds.form.realClientIp')}
  132. tooltip={t('pages.inbounds.form.realClientIpHint')}
  133. >
  134. <Segmented
  135. value={value}
  136. onChange={(v) =>
  137. applyRealClientIpPreset(v as RealClientIpPreset, getFieldValue, setFieldValue)
  138. }
  139. options={[
  140. { value: 'off', label: t('pages.inbounds.form.realClientIpPresetOff') },
  141. { value: 'cloudflare', label: t('pages.inbounds.form.realClientIpPresetCloudflare') },
  142. { value: 'proxy', label: t('pages.inbounds.form.realClientIpPresetProxyProtocol') },
  143. ]}
  144. />
  145. </Form.Item>
  146. {trustedMismatch && (
  147. <Alert
  148. type="warning"
  149. showIcon
  150. style={{ marginBottom: 16 }}
  151. message={t('pages.inbounds.form.realClientIpTrustedHeaderTransportWarn')}
  152. />
  153. )}
  154. {proxyMismatch && (
  155. <Alert
  156. type="warning"
  157. showIcon
  158. style={{ marginBottom: 16 }}
  159. message={t('pages.inbounds.form.realClientIpProxyProtocolTransportWarn')}
  160. />
  161. )}
  162. </>
  163. );
  164. }}
  165. </Form.Item>
  166. <Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
  167. <InputNumber min={0} />
  168. </Form.Item>
  169. <Form.Item
  170. name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
  171. label={t('pages.inbounds.form.tcpKeepAliveInterval')}
  172. >
  173. <InputNumber min={0} />
  174. </Form.Item>
  175. <Form.Item
  176. name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
  177. label={t('pages.inbounds.form.tcpKeepAliveIdle')}
  178. >
  179. <InputNumber min={0} />
  180. </Form.Item>
  181. <Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
  182. <InputNumber min={0} />
  183. </Form.Item>
  184. <Form.Item
  185. name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
  186. label={t('pages.inbounds.form.tcpUserTimeout')}
  187. >
  188. <InputNumber min={0} />
  189. </Form.Item>
  190. <Form.Item
  191. name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
  192. label={t('pages.inbounds.form.tcpWindowClamp')}
  193. tooltip={t('pages.inbounds.form.tcpWindowClampHint')}
  194. >
  195. <InputNumber min={0} />
  196. </Form.Item>
  197. <Form.Item
  198. name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
  199. label={t('pages.inbounds.form.proxyProtocol')}
  200. tooltip={t('pages.inbounds.form.proxyProtocolHint')}
  201. valuePropName="checked"
  202. >
  203. <Switch />
  204. </Form.Item>
  205. <Form.Item
  206. name={['streamSettings', 'sockopt', 'tcpFastOpen']}
  207. label={t('pages.inbounds.form.tcpFastOpen')}
  208. valuePropName="checked"
  209. >
  210. <Switch />
  211. </Form.Item>
  212. <Form.Item
  213. name={['streamSettings', 'sockopt', 'tcpMptcp']}
  214. label={t('pages.inbounds.form.multipathTcp')}
  215. valuePropName="checked"
  216. >
  217. <Switch />
  218. </Form.Item>
  219. <Form.Item
  220. name={['streamSettings', 'sockopt', 'penetrate']}
  221. label={t('pages.inbounds.form.penetrate')}
  222. valuePropName="checked"
  223. >
  224. <Switch />
  225. </Form.Item>
  226. <Form.Item
  227. name={['streamSettings', 'sockopt', 'V6Only']}
  228. label={t('pages.inbounds.form.v6Only')}
  229. valuePropName="checked"
  230. >
  231. <Switch />
  232. </Form.Item>
  233. <Form.Item
  234. name={['streamSettings', 'sockopt', 'domainStrategy']}
  235. label={t('pages.xray.wireguard.domainStrategy')}
  236. >
  237. <Select
  238. style={{ width: '50%' }}
  239. options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
  240. />
  241. </Form.Item>
  242. <Form.Item
  243. name={['streamSettings', 'sockopt', 'tcpcongestion']}
  244. label={t('pages.inbounds.form.tcpCongestion')}
  245. >
  246. <Select
  247. style={{ width: '50%' }}
  248. options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
  249. />
  250. </Form.Item>
  251. <Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
  252. <Select
  253. style={{ width: '50%' }}
  254. options={[
  255. { value: 'off', label: 'Off' },
  256. { value: 'redirect', label: 'Redirect' },
  257. { value: 'tproxy', label: 'TProxy' },
  258. ]}
  259. />
  260. </Form.Item>
  261. <Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label={t('pages.inbounds.form.dialerProxy')}>
  262. <Input />
  263. </Form.Item>
  264. <Form.Item
  265. name={['streamSettings', 'sockopt', 'interface']}
  266. label={t('pages.inbounds.info.interfaceName')}
  267. >
  268. <Input />
  269. </Form.Item>
  270. <Form.Item
  271. name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
  272. label={t('pages.inbounds.form.trustedXForwardedFor')}
  273. tooltip={t('pages.inbounds.form.trustedXForwardedForHint')}
  274. >
  275. <Select
  276. mode="tags"
  277. style={{ width: '100%' }}
  278. tokenSeparators={[',']}
  279. options={[
  280. { value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
  281. { value: 'X-Real-IP', label: 'X-Real-IP' },
  282. { value: 'True-Client-IP', label: 'True-Client-IP' },
  283. { value: 'X-Client-IP', label: 'X-Client-IP' },
  284. ]}
  285. />
  286. </Form.Item>
  287. <Form.Item
  288. name={['streamSettings', 'sockopt', 'addressPortStrategy']}
  289. label={t('pages.inbounds.form.addressPortStrategy')}
  290. >
  291. <Select
  292. style={{ width: '50%' }}
  293. options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
  294. />
  295. </Form.Item>
  296. <Form.Item shouldUpdate noStyle>
  297. {({ getFieldValue, setFieldValue }) => {
  298. const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
  299. const hasHe = he != null;
  300. return (
  301. <>
  302. <Form.Item label="Happy Eyeballs">
  303. <Switch
  304. checked={hasHe}
  305. onChange={(v) => {
  306. setFieldValue(
  307. ['streamSettings', 'sockopt', 'happyEyeballs'],
  308. v ? HappyEyeballsSchema.parse({}) : undefined,
  309. );
  310. }}
  311. />
  312. </Form.Item>
  313. {hasHe && (
  314. <>
  315. <Form.Item
  316. name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
  317. label={t('pages.inbounds.form.tryDelayMs')}
  318. >
  319. <InputNumber min={0} placeholder="0 disabled — 250 recommended" />
  320. </Form.Item>
  321. <Form.Item
  322. name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
  323. label={t('pages.inbounds.form.prioritizeIPv6')}
  324. valuePropName="checked"
  325. >
  326. <Switch />
  327. </Form.Item>
  328. <Form.Item
  329. name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
  330. label={t('pages.inbounds.form.interleave')}
  331. >
  332. <InputNumber min={1} />
  333. </Form.Item>
  334. <Form.Item
  335. name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
  336. label={t('pages.inbounds.form.maxConcurrentTry')}
  337. >
  338. <InputNumber min={0} />
  339. </Form.Item>
  340. </>
  341. )}
  342. </>
  343. );
  344. }}
  345. </Form.Item>
  346. <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
  347. {(fields, { add, remove }) => (
  348. <>
  349. <Form.Item label={t('pages.inbounds.form.customSockopt')}>
  350. <Button
  351. type="dashed"
  352. size="small"
  353. onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
  354. >
  355. + {t('pages.inbounds.form.addCustomOption')}
  356. </Button>
  357. </Form.Item>
  358. {fields.map((field) => (
  359. <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
  360. <Form.Item name={[field.name, 'system']} noStyle>
  361. <Select
  362. placeholder="all"
  363. allowClear
  364. style={{ width: 100 }}
  365. options={[
  366. { value: 'linux', label: 'linux' },
  367. { value: 'windows', label: 'windows' },
  368. { value: 'darwin', label: 'darwin' },
  369. ]}
  370. />
  371. </Form.Item>
  372. <Form.Item name={[field.name, 'type']} noStyle>
  373. <Select
  374. style={{ width: 80 }}
  375. options={[
  376. { value: 'int', label: 'int' },
  377. { value: 'str', label: 'str' },
  378. ]}
  379. />
  380. </Form.Item>
  381. <Form.Item name={[field.name, 'level']} noStyle>
  382. <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
  383. </Form.Item>
  384. <Form.Item name={[field.name, 'opt']} noStyle>
  385. <Input placeholder="opt" style={{ width: 120 }} />
  386. </Form.Item>
  387. <Form.Item name={[field.name, 'value']} noStyle>
  388. <Input placeholder="value" style={{ flex: 1 }} />
  389. </Form.Item>
  390. <Button danger onClick={() => remove(field.name)}>−</Button>
  391. </Space.Compact>
  392. ))}
  393. </>
  394. )}
  395. </Form.List>
  396. </>
  397. )}
  398. </>
  399. );
  400. }}
  401. </Form.Item>
  402. );
  403. }