sockopt.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. import { useTranslation } from 'react-i18next';
  2. import { Button, Form, Input, InputNumber, Select, Space, Switch, type FormInstance } from 'antd';
  3. import { DOMAIN_STRATEGY_OPTION, TCP_CONGESTION_OPTION } from '@/schemas/primitives';
  4. import { HappyEyeballsSchema, SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
  5. import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
  6. import { ADDRESS_PORT_STRATEGY_OPTIONS } from '../outbound-form-constants';
  7. export default function SockoptForm({
  8. form,
  9. outboundTags = [],
  10. }: {
  11. form: FormInstance<OutboundFormValues>;
  12. outboundTags?: string[];
  13. }) {
  14. const { t } = useTranslation();
  15. return (
  16. <Form.Item shouldUpdate noStyle>
  17. {() => {
  18. const hasSockopt = !!form.getFieldValue([
  19. 'streamSettings',
  20. 'sockopt',
  21. ]);
  22. const dialerProxy = (form.getFieldValue([
  23. 'streamSettings',
  24. 'sockopt',
  25. 'dialerProxy',
  26. ]) ?? '') as string;
  27. const dialerProxyOptions = Array.from(
  28. new Set([...outboundTags, dialerProxy].filter(Boolean)),
  29. ).map((tg) => ({ value: tg, label: tg }));
  30. return (
  31. <>
  32. <Form.Item label={t('pages.xray.outboundForm.sockopts')}>
  33. <Switch
  34. checked={hasSockopt}
  35. onChange={(checked) => {
  36. form.setFieldValue(
  37. ['streamSettings', 'sockopt'],
  38. checked ? SockoptStreamSettingsSchema.parse({}) : undefined,
  39. );
  40. }}
  41. />
  42. </Form.Item>
  43. {hasSockopt && (
  44. <>
  45. <Form.Item
  46. label={t('pages.inbounds.form.dialerProxy')}
  47. name={['streamSettings', 'sockopt', 'dialerProxy']}
  48. tooltip={t('pages.xray.outboundForm.dialerProxyHint')}
  49. >
  50. <Select
  51. allowClear
  52. showSearch
  53. placeholder={t('pages.xray.outboundForm.dialerProxyPlaceholder')}
  54. options={dialerProxyOptions}
  55. />
  56. </Form.Item>
  57. <Form.Item
  58. label={t('pages.xray.wireguard.domainStrategy')}
  59. name={['streamSettings', 'sockopt', 'domainStrategy']}
  60. >
  61. <Select
  62. options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({
  63. value: v,
  64. label: v,
  65. }))}
  66. />
  67. </Form.Item>
  68. <Form.Item
  69. label={t('pages.inbounds.form.addressPortStrategy')}
  70. name={['streamSettings', 'sockopt', 'addressPortStrategy']}
  71. >
  72. <Select options={ADDRESS_PORT_STRATEGY_OPTIONS} />
  73. </Form.Item>
  74. <Form.Item
  75. label={t('pages.xray.outboundForm.keepAliveInterval')}
  76. name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
  77. >
  78. <InputNumber min={0} />
  79. </Form.Item>
  80. <Form.Item
  81. label={t('pages.inbounds.form.tcpFastOpen')}
  82. name={['streamSettings', 'sockopt', 'tcpFastOpen']}
  83. valuePropName="checked"
  84. >
  85. <Switch />
  86. </Form.Item>
  87. <Form.Item
  88. label={t('pages.inbounds.form.multipathTcp')}
  89. name={['streamSettings', 'sockopt', 'tcpMptcp']}
  90. valuePropName="checked"
  91. >
  92. <Switch />
  93. </Form.Item>
  94. <Form.Item
  95. label={t('pages.inbounds.form.penetrate')}
  96. name={['streamSettings', 'sockopt', 'penetrate']}
  97. valuePropName="checked"
  98. >
  99. <Switch />
  100. </Form.Item>
  101. <Form.Item
  102. label={t('pages.xray.outboundForm.markFwmark')}
  103. name={['streamSettings', 'sockopt', 'mark']}
  104. >
  105. <InputNumber min={0} />
  106. </Form.Item>
  107. <Form.Item
  108. label={t('pages.xray.outboundForm.interface')}
  109. name={['streamSettings', 'sockopt', 'interface']}
  110. >
  111. <Input />
  112. </Form.Item>
  113. <Form.Item
  114. label="TProxy"
  115. name={['streamSettings', 'sockopt', 'tproxy']}
  116. >
  117. <Select
  118. options={[
  119. { value: 'off', label: 'off' },
  120. { value: 'redirect', label: 'redirect' },
  121. { value: 'tproxy', label: 'tproxy' },
  122. ]}
  123. />
  124. </Form.Item>
  125. <Form.Item
  126. label={t('pages.inbounds.form.tcpCongestion')}
  127. name={['streamSettings', 'sockopt', 'tcpcongestion']}
  128. >
  129. <Select
  130. options={Object.values(TCP_CONGESTION_OPTION).map((v) => ({
  131. value: v,
  132. label: v,
  133. }))}
  134. />
  135. </Form.Item>
  136. <Form.Item
  137. label={t('pages.xray.outboundForm.ipv6Only')}
  138. name={['streamSettings', 'sockopt', 'V6Only']}
  139. valuePropName="checked"
  140. >
  141. <Switch />
  142. </Form.Item>
  143. <Form.Item
  144. label={t('pages.xray.outboundForm.acceptProxyProtocol')}
  145. name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
  146. valuePropName="checked"
  147. >
  148. <Switch />
  149. </Form.Item>
  150. <Form.Item
  151. label={t('pages.xray.outboundForm.tcpUserTimeoutMs')}
  152. name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
  153. >
  154. <InputNumber min={0} style={{ width: '100%' }} />
  155. </Form.Item>
  156. <Form.Item
  157. label={t('pages.xray.outboundForm.tcpKeepAliveIdleS')}
  158. name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
  159. >
  160. <InputNumber min={0} style={{ width: '100%' }} />
  161. </Form.Item>
  162. <Form.Item
  163. label={t('pages.inbounds.form.tcpMaxSeg')}
  164. name={['streamSettings', 'sockopt', 'tcpMaxSeg']}
  165. >
  166. <InputNumber min={0} style={{ width: '100%' }} />
  167. </Form.Item>
  168. <Form.Item
  169. label={t('pages.inbounds.form.tcpWindowClamp')}
  170. name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
  171. tooltip={t('pages.inbounds.form.tcpWindowClampHint')}
  172. >
  173. <InputNumber min={0} style={{ width: '100%' }} />
  174. </Form.Item>
  175. <Form.Item
  176. label={t('pages.inbounds.form.trustedXForwardedFor')}
  177. name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
  178. >
  179. <Select
  180. mode="tags"
  181. tokenSeparators={[',', ' ']}
  182. placeholder="trusted-proxy.example,10.0.0.0/8"
  183. />
  184. </Form.Item>
  185. <Form.Item shouldUpdate noStyle>
  186. {() => {
  187. const he = form.getFieldValue([
  188. 'streamSettings', 'sockopt', 'happyEyeballs',
  189. ]);
  190. const hasHe = he != null;
  191. return (
  192. <>
  193. <Form.Item label="Happy Eyeballs">
  194. <Switch
  195. checked={hasHe}
  196. onChange={(v) => {
  197. form.setFieldValue(
  198. ['streamSettings', 'sockopt', 'happyEyeballs'],
  199. v ? HappyEyeballsSchema.parse({}) : undefined,
  200. );
  201. }}
  202. />
  203. </Form.Item>
  204. {hasHe && (
  205. <>
  206. <Form.Item
  207. label={t('pages.inbounds.form.tryDelayMs')}
  208. name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
  209. >
  210. <InputNumber min={0} style={{ width: '100%' }} placeholder="0 (disabled) — 250 recommended" />
  211. </Form.Item>
  212. <Form.Item
  213. label={t('pages.inbounds.form.prioritizeIPv6')}
  214. name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
  215. valuePropName="checked"
  216. >
  217. <Switch />
  218. </Form.Item>
  219. <Form.Item
  220. label={t('pages.inbounds.form.interleave')}
  221. name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
  222. >
  223. <InputNumber min={1} style={{ width: '100%' }} />
  224. </Form.Item>
  225. <Form.Item
  226. label={t('pages.inbounds.form.maxConcurrentTry')}
  227. name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
  228. >
  229. <InputNumber min={0} style={{ width: '100%' }} />
  230. </Form.Item>
  231. </>
  232. )}
  233. </>
  234. );
  235. }}
  236. </Form.Item>
  237. <Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
  238. {(fields, { add, remove }) => (
  239. <>
  240. <Form.Item label={t('pages.inbounds.form.customSockopt')}>
  241. <Button
  242. type="dashed"
  243. size="small"
  244. onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
  245. >
  246. + {t('pages.inbounds.form.addCustomOption')}
  247. </Button>
  248. </Form.Item>
  249. {fields.map((field) => (
  250. <Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
  251. <Form.Item name={[field.name, 'system']} noStyle>
  252. <Select
  253. placeholder="all"
  254. allowClear
  255. style={{ width: 100 }}
  256. options={[
  257. { value: 'linux', label: 'linux' },
  258. { value: 'windows', label: 'windows' },
  259. { value: 'darwin', label: 'darwin' },
  260. ]}
  261. />
  262. </Form.Item>
  263. <Form.Item name={[field.name, 'type']} noStyle>
  264. <Select
  265. style={{ width: 80 }}
  266. options={[
  267. { value: 'int', label: 'int' },
  268. { value: 'str', label: 'str' },
  269. ]}
  270. />
  271. </Form.Item>
  272. <Form.Item name={[field.name, 'level']} noStyle>
  273. <Input placeholder="level (6=TCP)" style={{ width: 100 }} />
  274. </Form.Item>
  275. <Form.Item name={[field.name, 'opt']} noStyle>
  276. <Input placeholder="opt (decimal)" style={{ width: 120 }} />
  277. </Form.Item>
  278. <Form.Item name={[field.name, 'value']} noStyle>
  279. <Input placeholder="value" style={{ flex: 1 }} />
  280. </Form.Item>
  281. <Button danger onClick={() => remove(field.name)}>−</Button>
  282. </Space.Compact>
  283. ))}
  284. </>
  285. )}
  286. </Form.List>
  287. </>
  288. )}
  289. </>
  290. );
  291. }}
  292. </Form.Item>
  293. );
  294. }