outbounds.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. // Pure builders for Xray outbound objects, matching the wire shapes 3x-ui
  2. // emits (internal/util/link/outbound.go + the panel's outbound-defaults.ts):
  3. // - VLESS uses the FLAT settings form {address,port,id,flow,encryption}.
  4. // - VMess uses the vnext form {vnext:[{address,port,users:[...]}]}.
  5. // - trojan/ss/socks/http use {servers:[...]}.
  6. // - freedom/blackhole use empty settings.
  7. // - wireguard/warp use {secretKey,address,peers:[...],mtu}.
  8. // No React/DOM imports — unit-tested in Node.
  9. export type OutboundKind =
  10. | 'freedom'
  11. | 'blackhole'
  12. | 'vless'
  13. | 'vmess'
  14. | 'trojan'
  15. | 'shadowsocks'
  16. | 'socks'
  17. | 'http'
  18. | 'wireguard'
  19. | 'warp';
  20. export type Network = 'tcp' | 'kcp' | 'ws' | 'grpc' | 'httpupgrade' | 'xhttp';
  21. export type Security = 'none' | 'tls' | 'reality';
  22. export interface StreamInput {
  23. network: Network;
  24. security: Security;
  25. host?: string;
  26. path?: string;
  27. serviceName?: string; // grpc
  28. sni?: string; // tls/reality serverName
  29. fingerprint?: string; // utls fingerprint
  30. publicKey?: string; // reality
  31. shortId?: string; // reality
  32. spiderX?: string; // reality
  33. }
  34. export interface ProxyServerInput {
  35. address: string;
  36. port: number;
  37. id?: string; // vless / vmess uuid
  38. password?: string; // trojan / ss / socks / http
  39. method?: string; // ss cipher
  40. flow?: string; // vless xtls flow
  41. encryption?: string; // vless, default 'none'
  42. vmessSecurity?: string; // vmess scy, default 'auto'
  43. username?: string; // socks / http
  44. }
  45. export interface WireguardInput {
  46. secretKey: string;
  47. address: string[]; // local interface addresses
  48. publicKey: string; // peer public key
  49. endpoint: string; // peer endpoint host:port
  50. preSharedKey?: string;
  51. reserved?: number[];
  52. mtu?: number;
  53. keepAlive?: number;
  54. }
  55. export interface OutboundInput {
  56. kind: OutboundKind;
  57. tag: string;
  58. server?: ProxyServerInput;
  59. wireguard?: WireguardInput;
  60. stream?: StreamInput;
  61. domainStrategy?: string; // freedom
  62. }
  63. // The well-known Cloudflare WARP WireGuard endpoint. The panel fills the real
  64. // keys/reserved bytes when you register WARP; this is the template default.
  65. const WARP_ENDPOINT = 'engage.cloudflareclient.com:2408';
  66. // Protocols that carry a streamSettings transport in the panel.
  67. const STREAM_KINDS = new Set<OutboundKind>(['vless', 'vmess', 'trojan', 'shadowsocks']);
  68. function toPort(port: number | undefined): number {
  69. const n = Number(port);
  70. return Number.isFinite(n) && n > 0 ? n : 443;
  71. }
  72. export function buildStreamSettings(s: StreamInput): Record<string, unknown> {
  73. const out: Record<string, unknown> = { network: s.network, security: s.security };
  74. switch (s.network) {
  75. case 'tcp':
  76. out.tcpSettings = { header: { type: 'none' } };
  77. break;
  78. case 'kcp':
  79. out.kcpSettings = {
  80. mtu: 1350,
  81. tti: 20,
  82. uplinkCapacity: 5,
  83. downlinkCapacity: 20,
  84. cwndMultiplier: 1,
  85. maxSendingWindow: 2097152,
  86. };
  87. break;
  88. case 'ws': {
  89. const ws: Record<string, unknown> = { path: s.path || '/' };
  90. if (s.host) ws.host = s.host;
  91. out.wsSettings = ws;
  92. break;
  93. }
  94. case 'grpc':
  95. out.grpcSettings = { serviceName: s.serviceName || '', multiMode: false };
  96. break;
  97. case 'httpupgrade': {
  98. const hu: Record<string, unknown> = { path: s.path || '/' };
  99. if (s.host) hu.host = s.host;
  100. out.httpupgradeSettings = hu;
  101. break;
  102. }
  103. case 'xhttp': {
  104. const xh: Record<string, unknown> = { path: s.path || '/', mode: 'auto' };
  105. if (s.host) xh.host = s.host;
  106. out.xhttpSettings = xh;
  107. break;
  108. }
  109. }
  110. if (s.security === 'tls') {
  111. const tls: Record<string, unknown> = {};
  112. if (s.sni) tls.serverName = s.sni;
  113. if (s.fingerprint) tls.fingerprint = s.fingerprint;
  114. out.tlsSettings = tls;
  115. } else if (s.security === 'reality') {
  116. const reality: Record<string, unknown> = { fingerprint: s.fingerprint || 'chrome' };
  117. if (s.publicKey) reality.publicKey = s.publicKey;
  118. if (s.sni) reality.serverName = s.sni;
  119. if (s.shortId) reality.shortId = s.shortId;
  120. if (s.spiderX) reality.spiderX = s.spiderX;
  121. out.realitySettings = reality;
  122. }
  123. return out;
  124. }
  125. function buildSettings(o: OutboundInput): Record<string, unknown> {
  126. const s = o.server;
  127. switch (o.kind) {
  128. case 'freedom':
  129. return o.domainStrategy ? { domainStrategy: o.domainStrategy } : {};
  130. case 'blackhole':
  131. return {};
  132. case 'vless':
  133. return {
  134. address: s?.address ?? '',
  135. port: toPort(s?.port),
  136. id: s?.id ?? '',
  137. flow: s?.flow ?? '',
  138. encryption: s?.encryption || 'none',
  139. };
  140. case 'vmess':
  141. return {
  142. vnext: [
  143. {
  144. address: s?.address ?? '',
  145. port: toPort(s?.port),
  146. users: [{ id: s?.id ?? '', security: s?.vmessSecurity || 'auto' }],
  147. },
  148. ],
  149. };
  150. case 'trojan':
  151. return { servers: [{ address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '' }] };
  152. case 'shadowsocks':
  153. return {
  154. servers: [
  155. {
  156. address: s?.address ?? '',
  157. port: toPort(s?.port),
  158. password: s?.password ?? '',
  159. method: s?.method || '2022-blake3-aes-128-gcm',
  160. },
  161. ],
  162. };
  163. case 'socks':
  164. case 'http': {
  165. const server: Record<string, unknown> = { address: s?.address ?? '', port: toPort(s?.port) };
  166. server.users = s?.username ? [{ user: s.username, pass: s.password ?? '' }] : [];
  167. return { servers: [server] };
  168. }
  169. case 'wireguard':
  170. case 'warp':
  171. return buildWireguardSettings(o);
  172. }
  173. }
  174. function buildWireguardSettings(o: OutboundInput): Record<string, unknown> {
  175. const w = o.wireguard;
  176. const isWarp = o.kind === 'warp';
  177. const peer: Record<string, unknown> = {
  178. publicKey: w?.publicKey ?? '',
  179. endpoint: w?.endpoint || (isWarp ? WARP_ENDPOINT : ''),
  180. allowedIPs: ['0.0.0.0/0', '::/0'],
  181. };
  182. if (w?.preSharedKey) peer.preSharedKey = w.preSharedKey;
  183. if (w?.keepAlive) peer.keepAlive = w.keepAlive;
  184. const settings: Record<string, unknown> = {
  185. secretKey: w?.secretKey ?? '',
  186. address: w?.address ?? [],
  187. peers: [peer],
  188. mtu: w?.mtu ?? 1420,
  189. };
  190. if (w?.reserved && w.reserved.length > 0) settings.reserved = w.reserved;
  191. return settings;
  192. }
  193. function protocolFor(kind: OutboundKind): string {
  194. return kind === 'warp' ? 'wireguard' : kind;
  195. }
  196. export function buildOutbound(o: OutboundInput): Record<string, unknown> {
  197. const tag = o.kind === 'warp' ? 'warp' : o.tag;
  198. const ob: Record<string, unknown> = {
  199. tag,
  200. protocol: protocolFor(o.kind),
  201. settings: buildSettings(o),
  202. };
  203. if (o.stream && STREAM_KINDS.has(o.kind)) {
  204. ob.streamSettings = buildStreamSettings(o.stream);
  205. }
  206. return ob;
  207. }
  208. export function buildOutboundJson(o: OutboundInput): string {
  209. return JSON.stringify(buildOutbound(o), null, 2);
  210. }