mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 22:26:09 +00:00
9b91f0f42e
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.
230 lines
6.8 KiB
TypeScript
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);
|
|
}
|