outbound-form.ts 11 KB

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