Files
3x-ui/docs/lib/xray/outbounds.ts
T
MHSanaei 9b91f0f42e docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to
docs.sanaei.dev) into docs/ so the panel and its documentation share a single
source of truth, the way sing-box keeps its docs in-tree. The old repo becomes
redundant and can be retired.

- Import the full site under docs/ (app, components, content, lib, public,
  scripts, config). The self-contained pnpm project sits alongside the existing
  engineering notes with no filename collisions.
- Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's
  docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx).
- Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to
  docs/** and run with working-directory: docs, since GitHub only runs
  workflows from the repo-root .github/. deploy-static.yml's GitHub Pages
  publish (CNAME docs.sanaei.dev) carries over unchanged.

Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to
this repository's Pages (or set the Vercel project's root directory to docs),
confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
2026-07-07 23:07:14 +02:00

230 lines
6.8 KiB
TypeScript

// 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<OutboundKind>(['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<string, unknown> {
const out: Record<string, unknown> = { 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<string, unknown> = { 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<string, unknown> = { path: s.path || '/' };
if (s.host) hu.host = s.host;
out.httpupgradeSettings = hu;
break;
}
case 'xhttp': {
const xh: Record<string, unknown> = { path: s.path || '/', mode: 'auto' };
if (s.host) xh.host = s.host;
out.xhttpSettings = xh;
break;
}
}
if (s.security === 'tls') {
const tls: Record<string, unknown> = {};
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<string, unknown> = { 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<string, unknown> {
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<string, unknown> = { 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<string, unknown> {
const w = o.wireguard;
const isWarp = o.kind === 'warp';
const peer: Record<string, unknown> = {
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<string, unknown> = {
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<string, unknown> {
const tag = o.kind === 'warp' ? 'warp' : o.tag;
const ob: Record<string, unknown> = {
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);
}