outbound-form.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { z } from 'zod';
  2. import { PortSchema } from '@/schemas/primitives';
  3. import { VmessSecuritySchema } from '@/schemas/protocols/inbound/vmess';
  4. import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
  5. import { SecuritySettingsSchema } from '@/schemas/protocols/security';
  6. import { NetworkSettingsSchema, StreamExtrasSchema } from '@/schemas/protocols/stream';
  7. import {
  8. BlackholeResponseTypeSchema,
  9. DNSRuleActionSchema,
  10. FreedomFinalRuleActionSchema,
  11. FreedomFragmentSchema,
  12. FreedomNoiseSchema,
  13. OutboundDomainStrategySchema,
  14. WireguardDomainStrategySchema,
  15. } from '@/schemas/protocols/outbound';
  16. // OutboundFormValues = the shape Form.useForm<T>() carries inside
  17. // OutboundFormModal. Differences from schemas/api wire schemas:
  18. //
  19. // - vmess vnext / trojan-ss-socks-http servers are FLATTENED into
  20. // {address, port, ...auth} at settings root. The adapter handles
  21. // nesting on submit.
  22. // - wireguard `address` (string[] wire) and `reserved` (number[] wire)
  23. // are comma-joined STRINGS in the form. The adapter splits + coerces.
  24. // - wireguard `pubKey` is a UI-only field derived from `secretKey`. Not
  25. // emitted on the wire — the adapter strips it.
  26. // - VLESS `reverseTag` and `reverseSniffing` are flat at settings root;
  27. // the adapter wraps them as { reverse: { tag, sniffing } } on the wire.
  28. // - blackhole `type` ('' | 'none' | 'http') is flat; the adapter wraps it
  29. // as { response: { type } } on the wire (omitted when empty).
  30. // - DNS rules carry `qtype` and `domain` as comma-joined strings (matches
  31. // the legacy DNSRule UI). The adapter normalizes them on submit.
  32. //
  33. // All flat-form settings types are documented inline so the adapter has a
  34. // single source of truth for the shape it converts between.
  35. // VMess outbound: connect target (address+port) + first user (id+security).
  36. // Wire: { vnext: [{ address, port, users: [{ id, security }] }] }.
  37. export const VmessOutboundFormSettingsSchema = z.object({
  38. address: z.string().default(''),
  39. port: PortSchema.default(443),
  40. id: z.string().default(''),
  41. security: VmessSecuritySchema.default('auto'),
  42. });
  43. export type VmessOutboundFormSettings = z.infer<typeof VmessOutboundFormSettingsSchema>;
  44. // Reverse-sniffing is only emitted when reverseTag is non-empty. Defaults
  45. // match legacy ReverseSniffing constructor.
  46. export const ReverseSniffingFormSchema = z.object({
  47. enabled: z.boolean().default(false),
  48. destOverride: z.array(z.string()).default(['http', 'tls', 'quic', 'fakedns']),
  49. metadataOnly: z.boolean().default(false),
  50. routeOnly: z.boolean().default(false),
  51. ipsExcluded: z.array(z.string()).default([]),
  52. domainsExcluded: z.array(z.string()).default([]),
  53. });
  54. export type ReverseSniffingForm = z.infer<typeof ReverseSniffingFormSchema>;
  55. // VLESS outbound: flat connect target + auth + Vision-specific knobs +
  56. // reverse-sniffing slice. testpre/testseed live behind canEnableVisionSeed.
  57. export const VlessOutboundFormSettingsSchema = z.object({
  58. address: z.string().default(''),
  59. port: PortSchema.default(443),
  60. id: z.string().default(''),
  61. flow: z.string().default(''),
  62. encryption: z.string().min(1).default('none'),
  63. reverseTag: z.string().default(''),
  64. reverseSniffing: ReverseSniffingFormSchema.default({
  65. enabled: false,
  66. destOverride: ['http', 'tls', 'quic', 'fakedns'],
  67. metadataOnly: false,
  68. routeOnly: false,
  69. ipsExcluded: [],
  70. domainsExcluded: [],
  71. }),
  72. testpre: z.number().int().min(0).default(0),
  73. testseed: z.array(z.number().int().positive()).default([]),
  74. });
  75. export type VlessOutboundFormSettings = z.infer<typeof VlessOutboundFormSettingsSchema>;
  76. export const TrojanOutboundFormSettingsSchema = z.object({
  77. address: z.string().default(''),
  78. port: PortSchema.default(443),
  79. password: z.string().default(''),
  80. });
  81. export type TrojanOutboundFormSettings = z.infer<typeof TrojanOutboundFormSettingsSchema>;
  82. export const ShadowsocksOutboundFormSettingsSchema = z.object({
  83. address: z.string().default(''),
  84. port: PortSchema.default(443),
  85. password: z.string().default(''),
  86. method: SSMethodSchema.default('2022-blake3-aes-128-gcm'),
  87. uot: z.boolean().default(false),
  88. UoTVersion: z.number().int().min(1).max(2).default(1),
  89. });
  90. export type ShadowsocksOutboundFormSettings = z.infer<typeof ShadowsocksOutboundFormSettingsSchema>;
  91. // SOCKS / HTTP: panel only supports a single server, with optionally one
  92. // user (the adapter emits users: [] when user is empty).
  93. export const SocksOutboundFormSettingsSchema = z.object({
  94. address: z.string().default(''),
  95. port: PortSchema.default(1080),
  96. user: z.string().default(''),
  97. pass: z.string().default(''),
  98. });
  99. export type SocksOutboundFormSettings = z.infer<typeof SocksOutboundFormSettingsSchema>;
  100. export const HttpOutboundFormSettingsSchema = z.object({
  101. address: z.string().default(''),
  102. port: PortSchema.default(8080),
  103. user: z.string().default(''),
  104. pass: z.string().default(''),
  105. });
  106. export type HttpOutboundFormSettings = z.infer<typeof HttpOutboundFormSettingsSchema>;
  107. // Wireguard peer mirrors the legacy Outbound.WireguardSettings.Peer class.
  108. // `psk` (form) <-> `preSharedKey` (wire) — adapter renames.
  109. export const WireguardOutboundFormPeerSchema = z.object({
  110. publicKey: z.string().default(''),
  111. psk: z.string().default(''),
  112. allowedIPs: z.array(z.string()).default(['0.0.0.0/0', '::/0']),
  113. endpoint: z.string().default(''),
  114. keepAlive: z.number().int().min(0).default(0),
  115. });
  116. export type WireguardOutboundFormPeer = z.infer<typeof WireguardOutboundFormPeerSchema>;
  117. // Wireguard: `address` and `reserved` are comma-joined strings in the form
  118. // (the legacy UI binds them to a single Input). pubKey is UI-only — the
  119. // modal derives it from secretKey via Wireguard.generateKeypair() and
  120. // displays it disabled; the adapter strips it.
  121. export const WireguardOutboundFormSettingsSchema = z.object({
  122. mtu: z.number().int().min(0).default(1420),
  123. secretKey: z.string().default(''),
  124. pubKey: z.string().default(''),
  125. address: z.string().default(''),
  126. workers: z.number().int().min(0).default(2),
  127. domainStrategy: z.union([WireguardDomainStrategySchema, z.literal('')]).default(''),
  128. reserved: z.string().default(''),
  129. peers: z.array(WireguardOutboundFormPeerSchema).default([]),
  130. noKernelTun: z.boolean().default(false),
  131. });
  132. export type WireguardOutboundFormSettings = z.infer<typeof WireguardOutboundFormSettingsSchema>;
  133. // Hysteria outbound carries the connect target only; transport-layer knobs
  134. // (auth, congestion, up/down, hop port, timeouts) ride on stream.hysteria.
  135. export const HysteriaOutboundFormSettingsSchema = z.object({
  136. address: z.string().default(''),
  137. port: PortSchema.default(443),
  138. version: z.literal(2).default(2),
  139. });
  140. export type HysteriaOutboundFormSettings = z.infer<typeof HysteriaOutboundFormSettingsSchema>;
  141. // FinalRule (freedom): network/port are strings; ip is string[]; blockDelay
  142. // is only meaningful when action === 'block'. The adapter omits empty
  143. // fields from the wire payload.
  144. export const FreedomFinalRuleFormSchema = z.object({
  145. action: FreedomFinalRuleActionSchema.default('block'),
  146. network: z.string().default(''),
  147. port: z.string().default(''),
  148. ip: z.array(z.string()).default([]),
  149. blockDelay: z.string().default(''),
  150. });
  151. export type FreedomFinalRuleForm = z.infer<typeof FreedomFinalRuleFormSchema>;
  152. export const FreedomOutboundFormSettingsSchema = z.object({
  153. domainStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
  154. redirect: z.string().default(''),
  155. fragment: FreedomFragmentSchema.default({
  156. packets: '1-3',
  157. length: '',
  158. interval: '',
  159. maxSplit: '',
  160. }),
  161. noises: z.array(FreedomNoiseSchema).default([]),
  162. finalRules: z.array(FreedomFinalRuleFormSchema).default([]),
  163. });
  164. export type FreedomOutboundFormSettings = z.infer<typeof FreedomOutboundFormSettingsSchema>;
  165. // Blackhole: legacy form keeps `type` as a flat string ('' | 'none' | 'http');
  166. // adapter wraps as { response: { type } } on the wire and omits when empty.
  167. export const BlackholeOutboundFormSettingsSchema = z.object({
  168. type: z.union([BlackholeResponseTypeSchema, z.literal('')]).default(''),
  169. });
  170. export type BlackholeOutboundFormSettings = z.infer<typeof BlackholeOutboundFormSettingsSchema>;
  171. // DNS rules: form holds qtype + domain as joined strings (the legacy UI
  172. // binds to <Input>). Adapter parses them on submit per the DNSRule class.
  173. export const DnsRuleFormSchema = z.object({
  174. action: DNSRuleActionSchema.default('direct'),
  175. qtype: z.string().default(''),
  176. domain: z.string().default(''),
  177. });
  178. export type DnsRuleForm = z.infer<typeof DnsRuleFormSchema>;
  179. export const DnsOutboundFormSettingsSchema = z.object({
  180. rewriteNetwork: z.union([z.enum(['udp', 'tcp']), z.literal('')]).default(''),
  181. rewriteAddress: z.string().default(''),
  182. rewritePort: z.number().int().min(0).max(65535).default(53),
  183. userLevel: z.number().int().min(0).default(0),
  184. rules: z.array(DnsRuleFormSchema).default([]),
  185. });
  186. export type DnsOutboundFormSettings = z.infer<typeof DnsOutboundFormSettingsSchema>;
  187. export const LoopbackOutboundFormSettingsSchema = z.object({
  188. inboundTag: z.string().default(''),
  189. });
  190. export type LoopbackOutboundFormSettings = z.infer<typeof LoopbackOutboundFormSettingsSchema>;
  191. // Discriminated union on `protocol`. Same tagged-wrapper pattern as the
  192. // inbound side: each branch is { protocol: literal, settings: <flat> }.
  193. export const OutboundFormSettingsSchema = z.discriminatedUnion('protocol', [
  194. z.object({ protocol: z.literal('vmess'), settings: VmessOutboundFormSettingsSchema }),
  195. z.object({ protocol: z.literal('vless'), settings: VlessOutboundFormSettingsSchema }),
  196. z.object({ protocol: z.literal('trojan'), settings: TrojanOutboundFormSettingsSchema }),
  197. z.object({ protocol: z.literal('shadowsocks'), settings: ShadowsocksOutboundFormSettingsSchema }),
  198. z.object({ protocol: z.literal('socks'), settings: SocksOutboundFormSettingsSchema }),
  199. z.object({ protocol: z.literal('http'), settings: HttpOutboundFormSettingsSchema }),
  200. z.object({ protocol: z.literal('wireguard'), settings: WireguardOutboundFormSettingsSchema }),
  201. z.object({ protocol: z.literal('hysteria'), settings: HysteriaOutboundFormSettingsSchema }),
  202. z.object({ protocol: z.literal('freedom'), settings: FreedomOutboundFormSettingsSchema }),
  203. z.object({ protocol: z.literal('blackhole'), settings: BlackholeOutboundFormSettingsSchema }),
  204. z.object({ protocol: z.literal('dns'), settings: DnsOutboundFormSettingsSchema }),
  205. z.object({ protocol: z.literal('loopback'), settings: LoopbackOutboundFormSettingsSchema }),
  206. ]);
  207. export type OutboundFormSettings = z.infer<typeof OutboundFormSettingsSchema>;
  208. // Mux ride: only emitted when enabled. The adapter respects canEnableMux
  209. // (gated by protocol + flow + network).
  210. export const MuxFormSchema = z.object({
  211. enabled: z.boolean().default(false),
  212. concurrency: z.number().int().default(8),
  213. xudpConcurrency: z.number().int().default(16),
  214. xudpProxyUDP443: z.enum(['reject', 'allow', 'skip']).default('reject'),
  215. });
  216. export type MuxForm = z.infer<typeof MuxFormSchema>;
  217. // Stream form mirrors the inbound side: NetworkSettings DU + SecuritySettings
  218. // DU + extras (sockopt). Hysteria gets a side-channel branch in the modal
  219. // (legacy ob.stream.hysteria) — keeping the DU strict for now and routing
  220. // hysteria transport knobs through the Advanced JSON tab if needed.
  221. export const OutboundStreamFormSchema = NetworkSettingsSchema
  222. .and(SecuritySettingsSchema)
  223. .and(StreamExtrasSchema);
  224. export type OutboundStreamFormValues = z.infer<typeof OutboundStreamFormSchema>;
  225. // Top-level form base: identity (tag, sendThrough), then the per-protocol
  226. // settings DU, then the stream sub-form, then mux.
  227. export const OutboundFormBaseSchema = z.object({
  228. tag: z.string().default(''),
  229. sendThrough: z.string().default(''),
  230. streamSettings: OutboundStreamFormSchema.optional(),
  231. mux: MuxFormSchema.default({
  232. enabled: false,
  233. concurrency: 8,
  234. xudpConcurrency: 16,
  235. xudpProxyUDP443: 'reject',
  236. }),
  237. });
  238. export type OutboundFormBase = z.infer<typeof OutboundFormBaseSchema>;
  239. // Full form values = base + protocol-discriminated settings. Consumers
  240. // narrow on `.protocol` to access the matching settings branch.
  241. export const OutboundFormSchema = OutboundFormBaseSchema.and(OutboundFormSettingsSchema);
  242. export type OutboundFormValues = z.infer<typeof OutboundFormSchema>;