Skip to content
Merged

One #51

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
889 changes: 889 additions & 0 deletions lib/backend/proxy/__tests__/complex-protocol.test.ts

Large diffs are not rendered by default.

1,540 changes: 1,540 additions & 0 deletions lib/backend/proxy/__tests__/comprehensive-all-protocols-all-formats.test.ts

Large diffs are not rendered by default.

57 changes: 56 additions & 1 deletion lib/backend/proxy/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ export const PROXY_PROTOCOLS = [
/**
* 传输协议
*/
export const NETWORK_TYPES = ['tcp', 'ws', 'grpc', 'h2', 'http', 'kcp', 'quic'] as const;
export const NETWORK_TYPES = [
'tcp',
'ws', // WebSocket
'grpc', // gRPC
'h2', // HTTP/2
'http', // HTTP
'kcp', // mKCP
'quic', // QUIC
'xhttp', // XHTTP/SplitHTTP (Xray-core)
'splithttp' // SplitHTTP 别名
] as const;

// ============================================================================
// 平台常量
Expand Down Expand Up @@ -147,6 +157,11 @@ export const VLESS_FLOWS = [
'xtls-rprx-vision-udp443'
] as const;

/**
* Hysteria 混淆类型
*/
export const HYSTERIA_OBFS = ['salamander'] as const;

/**
* Hysteria2 混淆类型
*/
Expand All @@ -162,6 +177,46 @@ export const TUIC_CONGESTION_CONTROLLERS = ['cubic', 'new_reno', 'bbr'] as const
*/
export const ALPN_PROTOCOLS = ['h3', 'h2', 'http/1.1'] as const;

/**
* VLESS Reality Xray 模式
*/
export const XHTTP_MODES = ['auto', 'packet-up', 'stream-up', 'stream-one'] as const;

/**
* mKCP 伪装头部类型
*/
export const KCP_HEADER_TYPES = ['none', 'srtp', 'utp', 'wechat-video', 'dtls', 'wireguard'] as const;

/**
* VMess Security 别名映射
* 用于兼容不同客户端的加密方法命名
*/
export const VMESS_SECURITY_ALIASES: Record<string, string> = {
'aes-128-gcm': 'aes-128-gcm',
'chacha20-poly1305': 'chacha20-poly1305',
'auto': 'auto',
'none': 'none',
'zero': 'zero',
// Shadowrocket 兼容
'aes_128_gcm': 'aes-128-gcm',
'chacha20_poly1305': 'chacha20-poly1305',
// 旧版兼容 -> 统一到 auto
'aes-128-cfb': 'auto',
'aes-128-ctr': 'auto',
'rc4-md5': 'auto',
'aes-128-gcm-ipv6': 'aes-128-gcm',
'chacha20-poly1305-ipv6': 'chacha20-poly1305'
};

/**
* 标准化 VMess 加密方法
*/
export function normalizeVmessSecurity(security: string | undefined): string {
if (!security) return 'auto';
const normalized = security.toLowerCase().trim();
return VMESS_SECURITY_ALIASES[normalized] || 'auto';
}

// ============================================================================
// 默认值
// ============================================================================
Expand Down
88 changes: 88 additions & 0 deletions lib/backend/proxy/converter/clash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,23 @@ export class ClashConverter extends BaseConverter {
if (node.cipher && !vmessCiphers.includes(node.cipher)) {
node.cipher = 'auto';
}
} else if (node.type === 'ss') {
if (isPresent(node, 'shadow-tls-password') && !isPresent(node, 'plugin')) {
node.plugin = 'shadow-tls';
node['plugin-opts'] = {
host: node['shadow-tls-sni'],
password: node['shadow-tls-password'],
version: node['shadow-tls-version']
};
delete node['shadow-tls-password'];
delete node['shadow-tls-sni'];
delete node['shadow-tls-version'];
}
if (isPresent(node, 'plugin-opts.mux') && node['plugin-opts']?.mux !== undefined) {
const v = String(node['plugin-opts'].mux).toLowerCase();
if (v === 'true' || v === '1') node['plugin-opts'].mux = true;
else if (v === 'false' || v === '0') node['plugin-opts'].mux = false;
}
} else if (node.type === 'vless') {
node.cipher = 'none'; // VLESS always none in Clash Meta
if (node['reality-opts']) {
Expand All @@ -155,6 +172,23 @@ export class ClashConverter extends BaseConverter {
'spider-x': node['reality-opts']['_spider-x'] || ''
};
}
if (node['ech-opts'] && node['ech-opts']['config-list']) {
node['ech-opts'] = {
'config-list': node['ech-opts']['config-list']
};
}
if (node['xhttp-opts']) {
const xopts: any = {};
const rawOpts = node['xhttp-opts'] as Record<string, any>;
if (rawOpts.mode) xopts.mode = rawOpts.mode;
if (rawOpts.path) xopts.path = rawOpts.path;
if (rawOpts.host) xopts.host = rawOpts.host;
if (rawOpts.headers) xopts.headers = rawOpts.headers;
if (rawOpts['download-settings']) xopts['download-settings'] = rawOpts['download-settings'];
if (Object.keys(xopts).length > 0) {
node['xhttp-opts'] = xopts;
}
}
} else if (node.type === 'wireguard') {
node.keepalive = node.keepalive ?? node['persistent-keepalive'];
node['persistent-keepalive'] = node.keepalive;
Expand Down Expand Up @@ -184,6 +218,10 @@ export class ClashConverter extends BaseConverter {
}
}

if (['trojan', 'tuic', 'hysteria', 'hysteria2', 'anytls', 'naive'].includes(node.type)) {
delete node.tls;
}

if (node['client-fingerprint']) {
node['client-fingerprint'] = node['client-fingerprint'];
} else if (['vmess', 'vless', 'trojan'].includes(node.type)) {
Expand Down Expand Up @@ -228,6 +266,32 @@ export class ClashConverter extends BaseConverter {
) {
node['http-opts'].headers.Host = [node['http-opts'].headers.Host];
}
} else if (
(node.type === 'vmess' || node.type === 'vless') &&
node.network === 'h2'
) {
if (node['h2-opts']) {
// h2-opts.path: array → first element
const h2Path = node['h2-opts'].path;
if (isPresent(node, 'h2-opts.path') && Array.isArray(h2Path)) {
node['h2-opts'].path = h2Path[0];
}
// h2-opts.host: 从 headers.host 或 host 提取,输出为数组
const h2Host = node['h2-opts'].host ??
node['h2-opts'].headers?.host ??
node['h2-opts'].headers?.Host;
if (isPresent(node, 'h2-opts.host') || isPresent(node, 'h2-opts.headers.host') || isPresent(node, 'h2-opts.headers.Host')) {
node['h2-opts'].host = Array.isArray(h2Host) ? h2Host : [h2Host];
}
// 清理 headers.host/Host
if (node['h2-opts'].headers) {
delete node['h2-opts'].headers.host;
delete node['h2-opts'].headers.Host;
if (Object.keys(node['h2-opts'].headers).length === 0) {
delete node['h2-opts'].headers;
}
}
}
} else if (node.network === 'kcp') {
// mKCP 传输层处理
node['kcp-opts'] = node['kcp-opts'] || {};
Expand Down Expand Up @@ -265,12 +329,32 @@ export class ClashConverter extends BaseConverter {
// Cleanup
delete node.subName;
delete node.id;
delete node.resolved;

if (node['tls-fingerprint']) {
node.fingerprint = node['tls-fingerprint'];
}
delete node['tls-fingerprint'];

for (const key in node) {
if (key.startsWith('_') || node[key] === null) {
delete node[key];
}
}

if (node.network === 'ws' && node['ws-opts']) {
const wsOpts = node['ws-opts'];
if (wsOpts['v2ray-http-upgrade'] !== undefined) delete wsOpts['v2ray-http-upgrade'];
if (wsOpts['v2ray-http-upgrade-fast-open'] !== undefined) delete wsOpts['v2ray-http-upgrade-fast-open'];
if (wsOpts['_v2ray-http-upgrade-ed'] !== undefined) delete wsOpts['_v2ray-http-upgrade-ed'];
}
if (node.httpupgrade !== undefined) delete node.httpupgrade;

if (node.network === 'grpc' && node['grpc-opts']) {
delete node['grpc-opts']['_grpc-type'];
delete node['grpc-opts']['_grpc-authority'];
}

// === 按照 Clash 标准重新排序字段 ===
// 确保输出的 YAML 符合 Clash 规范: name, type, server, port 在最前面
const orderedNode: any = {
Expand Down Expand Up @@ -300,6 +384,8 @@ export class ClashConverter extends BaseConverter {
'flow',
// Reality
'reality-opts',
// ECH (ClashMeta)
'ech-opts',
// WebSocket
'ws-opts',
'ws-path',
Expand All @@ -313,6 +399,8 @@ export class ClashConverter extends BaseConverter {
'kcp-opts',
// QUIC
'quic-opts',
// XHTTP/SplitHTTP (ClashMeta)
'xhttp-opts',
// 协议特定
'auth',
'auth-str',
Expand Down
Loading