// Pure builders for Xray outbound objects, matching the wire shapes 3x-ui // emits (internal/util/link/outbound.go + the panel's outbound-defaults.ts): // - VLESS uses the FLAT settings form {address,port,id,flow,encryption}. // - VMess uses the vnext form {vnext:[{address,port,users:[...]}]}. // - trojan/ss/socks/http use {servers:[...]}. // - freedom/blackhole use empty settings. // - wireguard/warp use {secretKey,address,peers:[...],mtu}. // No React/DOM imports — unit-tested in Node. export type OutboundKind = | 'freedom' | 'blackhole' | 'vless' | 'vmess' | 'trojan' | 'shadowsocks' | 'socks' | 'http' | 'wireguard' | 'warp'; export type Network = 'tcp' | 'kcp' | 'ws' | 'grpc' | 'httpupgrade' | 'xhttp'; export type Security = 'none' | 'tls' | 'reality'; export interface StreamInput { network: Network; security: Security; host?: string; path?: string; serviceName?: string; // grpc sni?: string; // tls/reality serverName fingerprint?: string; // utls fingerprint publicKey?: string; // reality shortId?: string; // reality spiderX?: string; // reality } export interface ProxyServerInput { address: string; port: number; id?: string; // vless / vmess uuid password?: string; // trojan / ss / socks / http method?: string; // ss cipher flow?: string; // vless xtls flow encryption?: string; // vless, default 'none' vmessSecurity?: string; // vmess scy, default 'auto' username?: string; // socks / http } export interface WireguardInput { secretKey: string; address: string[]; // local interface addresses publicKey: string; // peer public key endpoint: string; // peer endpoint host:port preSharedKey?: string; reserved?: number[]; mtu?: number; keepAlive?: number; } export interface OutboundInput { kind: OutboundKind; tag: string; server?: ProxyServerInput; wireguard?: WireguardInput; stream?: StreamInput; domainStrategy?: string; // freedom } // The well-known Cloudflare WARP WireGuard endpoint. The panel fills the real // keys/reserved bytes when you register WARP; this is the template default. const WARP_ENDPOINT = 'engage.cloudflareclient.com:2408'; // Protocols that carry a streamSettings transport in the panel. const STREAM_KINDS = new Set(['vless', 'vmess', 'trojan', 'shadowsocks']); function toPort(port: number | undefined): number { const n = Number(port); return Number.isFinite(n) && n > 0 ? n : 443; } export function buildStreamSettings(s: StreamInput): Record { const out: Record = { network: s.network, security: s.security }; switch (s.network) { case 'tcp': out.tcpSettings = { header: { type: 'none' } }; break; case 'kcp': out.kcpSettings = { mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20, cwndMultiplier: 1, maxSendingWindow: 2097152, }; break; case 'ws': { const ws: Record = { path: s.path || '/' }; if (s.host) ws.host = s.host; out.wsSettings = ws; break; } case 'grpc': out.grpcSettings = { serviceName: s.serviceName || '', multiMode: false }; break; case 'httpupgrade': { const hu: Record = { path: s.path || '/' }; if (s.host) hu.host = s.host; out.httpupgradeSettings = hu; break; } case 'xhttp': { const xh: Record = { path: s.path || '/', mode: 'auto' }; if (s.host) xh.host = s.host; out.xhttpSettings = xh; break; } } if (s.security === 'tls') { const tls: Record = {}; if (s.sni) tls.serverName = s.sni; if (s.fingerprint) tls.fingerprint = s.fingerprint; out.tlsSettings = tls; } else if (s.security === 'reality') { const reality: Record = { fingerprint: s.fingerprint || 'chrome' }; if (s.publicKey) reality.publicKey = s.publicKey; if (s.sni) reality.serverName = s.sni; if (s.shortId) reality.shortId = s.shortId; if (s.spiderX) reality.spiderX = s.spiderX; out.realitySettings = reality; } return out; } function buildSettings(o: OutboundInput): Record { const s = o.server; switch (o.kind) { case 'freedom': return o.domainStrategy ? { domainStrategy: o.domainStrategy } : {}; case 'blackhole': return {}; case 'vless': return { address: s?.address ?? '', port: toPort(s?.port), id: s?.id ?? '', flow: s?.flow ?? '', encryption: s?.encryption || 'none', }; case 'vmess': return { vnext: [ { address: s?.address ?? '', port: toPort(s?.port), users: [{ id: s?.id ?? '', security: s?.vmessSecurity || 'auto' }], }, ], }; case 'trojan': return { servers: [{ address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '' }] }; case 'shadowsocks': return { servers: [ { address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '', method: s?.method || '2022-blake3-aes-128-gcm', }, ], }; case 'socks': case 'http': { const server: Record = { address: s?.address ?? '', port: toPort(s?.port) }; server.users = s?.username ? [{ user: s.username, pass: s.password ?? '' }] : []; return { servers: [server] }; } case 'wireguard': case 'warp': return buildWireguardSettings(o); } } function buildWireguardSettings(o: OutboundInput): Record { const w = o.wireguard; const isWarp = o.kind === 'warp'; const peer: Record = { publicKey: w?.publicKey ?? '', endpoint: w?.endpoint || (isWarp ? WARP_ENDPOINT : ''), allowedIPs: ['0.0.0.0/0', '::/0'], }; if (w?.preSharedKey) peer.preSharedKey = w.preSharedKey; if (w?.keepAlive) peer.keepAlive = w.keepAlive; const settings: Record = { secretKey: w?.secretKey ?? '', address: w?.address ?? [], peers: [peer], mtu: w?.mtu ?? 1420, }; if (w?.reserved && w.reserved.length > 0) settings.reserved = w.reserved; return settings; } function protocolFor(kind: OutboundKind): string { return kind === 'warp' ? 'wireguard' : kind; } export function buildOutbound(o: OutboundInput): Record { const tag = o.kind === 'warp' ? 'warp' : o.tag; const ob: Record = { tag, protocol: protocolFor(o.kind), settings: buildSettings(o), }; if (o.stream && STREAM_KINDS.has(o.kind)) { ob.streamSettings = buildStreamSettings(o.stream); } return ob; } export function buildOutboundJson(o: OutboundInput): string { return JSON.stringify(buildOutbound(o), null, 2); }