@herbertgao/surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
448 lines • 20.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getClashNodeNames = exports.getClashNodes = void 0;
const logger_1 = require("@surgio/logger");
const lodash_1 = __importDefault(require("lodash"));
const constant_1 = require("../constant");
const types_1 = require("../types");
const filters_1 = require("../filters");
const index_1 = require("./index");
const logger = (0, logger_1.createLogger)({ service: 'surgio:utils:clash' });
const getClashNodes = function (list, filter) {
return (0, filters_1.applyFilter)(list, filter)
.map((nodeConfig) => {
const clashNode = nodeListMapper(nodeConfig);
if (!clashNode) {
return clashNode;
}
if (nodeConfig?.clashConfig?.clashCore === 'clash.meta') {
if (nodeConfig.underlyingProxy) {
clashNode['dialer-proxy'] = nodeConfig.underlyingProxy;
}
if (nodeConfig.ipVersion) {
clashNode['ip-version'] = nodeConfig.ipVersion;
}
if (nodeConfig.interfaceName) {
clashNode['interface-name'] = nodeConfig.interfaceName;
}
if ('multiplex' in nodeConfig && nodeConfig.multiplex) {
// https://wiki.metacubex.one/config/proxies/sing-mux/#sing-mux
clashNode.smux = {
enabled: true,
protocol: nodeConfig.multiplex.protocol,
...(nodeConfig.multiplex.brutal && {
'brutal-opts': {
enabled: true,
up: nodeConfig.multiplex.brutal.upMbps,
down: nodeConfig.multiplex.brutal.downMbps,
},
}),
...(0, index_1.pickAndFormatKeys)(nodeConfig.multiplex, ['maxConnections', 'minStreams', 'maxStreams', 'padding'], {
keyFormat: 'kebabCase',
}),
};
}
}
return clashNode;
})
.filter((item) => (0, index_1.checkNotNullish)(item));
};
exports.getClashNodes = getClashNodes;
const getClashNodeNames = function (list, filter, prependNodeNames, defaultNodeNames) {
// istanbul ignore next
if (arguments.length === 2 && typeof filter === 'undefined') {
throw new Error(constant_1.ERR_INVALID_FILTER);
}
let result = [];
if (prependNodeNames) {
result = result.concat(prependNodeNames);
}
result = result.concat((0, exports.getClashNodes)(list, filter).map((item) => item.name));
if (result.length === 0 && defaultNodeNames) {
result = result.concat(defaultNodeNames);
}
return result;
};
exports.getClashNodeNames = getClashNodeNames;
/**
* @see https://wiki.metacubex.one/config/proxies/
* @see https://stash.wiki/proxy-protocols/proxy-types
*/
function nodeListMapper(nodeConfig) {
const clashConfig = nodeConfig.clashConfig || {};
switch (nodeConfig.type) {
case types_1.NodeTypeEnum.Shadowsocks:
// Istanbul ignore next
if (nodeConfig.shadowTls && !clashConfig.enableShadowTls) {
logger.warn(`尚未开启 Clash 的 shadow-tls 支持,节点 ${nodeConfig.nodeName} 将被忽略。如需开启,请在配置文件中设置 clashConfig.enableShadowTls 为 true。`);
return null;
}
// Istanbul ignore next
if (nodeConfig.shadowTls && nodeConfig.obfs) {
logger.warn(`Clash 不支持同时开启 shadow-tls 和 obfs,节点 ${nodeConfig.nodeName} 将被忽略。`);
return null;
}
return {
type: 'ss',
cipher: nodeConfig.method,
name: nodeConfig.nodeName,
password: nodeConfig.password,
port: nodeConfig.port,
server: nodeConfig.hostname,
udp: nodeConfig.udpRelay === true,
...(nodeConfig.obfs && ['tls', 'http'].includes(nodeConfig.obfs)
? {
plugin: 'obfs',
'plugin-opts': {
mode: nodeConfig.obfs,
host: nodeConfig.obfsHost,
},
}
: null),
...(nodeConfig.obfs && ['ws', 'wss'].includes(nodeConfig.obfs)
? {
plugin: 'v2ray-plugin',
'plugin-opts': {
mode: 'websocket',
tls: nodeConfig.obfs === 'wss',
...(typeof nodeConfig.skipCertVerify === 'boolean' &&
nodeConfig.obfs === 'wss'
? {
'skip-cert-verify': nodeConfig.skipCertVerify,
}
: null),
host: nodeConfig.obfsHost,
path: nodeConfig.obfsUri || '/',
mux: typeof nodeConfig.mux === 'boolean' ? nodeConfig.mux : false,
headers: lodash_1.default.omit(nodeConfig.wsHeaders || {}, ['host']),
},
}
: null),
...(nodeConfig.shadowTls && nodeConfig.clashConfig?.enableShadowTls
? {
plugin: 'shadow-tls',
'client-fingerprint': 'chrome',
'plugin-opts': {
password: nodeConfig.shadowTls.password,
...(nodeConfig.shadowTls.version
? { version: nodeConfig.shadowTls.version }
: null),
...(nodeConfig.shadowTls.sni
? { host: nodeConfig.shadowTls.sni }
: null),
},
}
: null),
};
case types_1.NodeTypeEnum.Vless:
case types_1.NodeTypeEnum.Vmess: {
if (nodeConfig.type === 'vless' && !clashConfig.enableVless) {
logger.warn(`尚未开启 Clash 的 VLESS 支持,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableVless 为 true。`);
return null;
}
const vmessNode = {
type: nodeConfig.type,
cipher: nodeConfig.method,
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
udp: nodeConfig.udpRelay === true,
uuid: nodeConfig.uuid,
network: nodeConfig.network || 'tcp',
};
if (nodeConfig.type === types_1.NodeTypeEnum.Vmess) {
vmessNode.alterId = nodeConfig.alterId || '0';
}
if (nodeConfig.type === types_1.NodeTypeEnum.Vless) {
vmessNode.flow = nodeConfig.flow;
if (nodeConfig.realityOpts) {
vmessNode['reality-opts'] = {
'public-key': nodeConfig.realityOpts.publicKey,
};
if (nodeConfig.realityOpts.shortId) {
vmessNode['reality-opts']['short-id'] =
nodeConfig.realityOpts.shortId;
}
if (nodeConfig.realityOpts.spiderX) {
vmessNode['reality-opts']['spider-x'] =
nodeConfig.realityOpts.spiderX;
}
}
}
if ((nodeConfig.type === types_1.NodeTypeEnum.Vmess && nodeConfig.tls) ||
nodeConfig.type === types_1.NodeTypeEnum.Vless) {
vmessNode.tls = true;
if (nodeConfig.skipCertVerify) {
vmessNode['skip-cert-verify'] = nodeConfig.skipCertVerify;
}
if (clashConfig.clashCore === 'clash' && nodeConfig.sni) {
vmessNode.servername = nodeConfig.sni;
}
if (clashConfig.clashCore === 'stash' && nodeConfig.sni) {
vmessNode.sni = nodeConfig.sni;
vmessNode.servername = nodeConfig.sni;
}
if (clashConfig.clashCore === 'clash.meta' && nodeConfig.sni) {
vmessNode.servername = nodeConfig.sni;
}
if (nodeConfig.clientFingerprint) {
vmessNode['client-fingerprint'] = nodeConfig.clientFingerprint;
}
}
switch (nodeConfig.network) {
case 'tcp':
break;
case 'ws':
vmessNode['ws-opts'] = nodeConfig.wsOpts;
break;
case 'h2':
vmessNode['h2-opts'] = nodeConfig.h2Opts;
break;
case 'http':
vmessNode['http-opts'] = {
...nodeConfig.httpOpts,
headers: resolveVmessHttpHeadersFromSurgioConfig(nodeConfig.httpOpts?.headers || {}),
};
break;
case 'grpc':
if (nodeConfig.grpcOpts) {
vmessNode['grpc-opts'] = {
'grpc-service-name': nodeConfig.grpcOpts.serviceName,
};
}
break;
}
return vmessNode;
}
case types_1.NodeTypeEnum.Shadowsocksr:
return {
type: 'ssr',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
password: nodeConfig.password,
obfs: nodeConfig.obfs,
protocol: nodeConfig.protocol,
cipher: nodeConfig.method,
'obfs-param': nodeConfig.obfsparam ?? '',
'protocol-param': nodeConfig.protoparam ?? '',
udp: nodeConfig.udpRelay === true,
};
case types_1.NodeTypeEnum.Snell:
// Istanbul ignore next
if (Number(nodeConfig.version) >= 4) {
logger.warn(`Clash 尚不支持 Snell v${nodeConfig.version},节点 ${nodeConfig.nodeName} 会被省略。`);
return null;
}
return {
type: 'snell',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
psk: nodeConfig.psk,
'obfs-opts': {
mode: nodeConfig.obfs,
...(nodeConfig.obfsHost
? {
host: nodeConfig.obfsHost,
}
: null),
},
...(nodeConfig.version
? {
version: nodeConfig.version,
}
: null),
};
case types_1.NodeTypeEnum.HTTPS:
return {
type: 'http',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
username: nodeConfig.username /* istanbul ignore next */ || '',
password: nodeConfig.password /* istanbul ignore next */ || '',
tls: true,
'skip-cert-verify': nodeConfig.skipCertVerify === true,
};
case types_1.NodeTypeEnum.HTTP:
return {
type: 'http',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
username: nodeConfig.username /* istanbul ignore next */ || '',
password: nodeConfig.password /* istanbul ignore next */ || '',
};
case types_1.NodeTypeEnum.Trojan:
return {
type: 'trojan',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
password: nodeConfig.password,
...(nodeConfig.udpRelay ? { udp: nodeConfig.udpRelay } : null),
...(nodeConfig.alpn ? { alpn: nodeConfig.alpn } : null),
...(nodeConfig.sni ? { sni: nodeConfig.sni } : null),
'skip-cert-verify': nodeConfig.skipCertVerify === true,
...(nodeConfig.network === 'ws'
? {
network: 'ws',
'ws-opts': {
path: nodeConfig.wsPath || '/',
...nodeConfig.wsHeaders,
},
}
: null),
};
case types_1.NodeTypeEnum.Socks5:
return {
type: 'socks5',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
...(nodeConfig.username ? { username: nodeConfig.username } : null),
...(nodeConfig.password ? { password: nodeConfig.password } : null),
...(typeof nodeConfig.tls === 'boolean'
? { tls: nodeConfig.tls }
: null),
...(typeof nodeConfig.skipCertVerify === 'boolean'
? { 'skip-cert-verify': nodeConfig.skipCertVerify }
: null),
...(typeof nodeConfig.udpRelay === 'boolean'
? { udp: nodeConfig.udpRelay }
: null),
};
case types_1.NodeTypeEnum.Tuic:
// Istanbul ignore next
if (!clashConfig.enableTuic) {
logger.warn(`尚未开启 Clash 的 Tuic 支持,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableTuic 为 true。`);
return null;
}
// Istanbul ignore next
if (nodeConfig.alpn && !nodeConfig.alpn.length) {
logger.warn(`节点 ${nodeConfig.nodeName} 的 alpn 为空。Stash 客户端不支持 ALPN 为空,默认的 ALPN 为 h3。`);
}
if ('version' in nodeConfig && Number(nodeConfig.version) >= 5) {
return {
type: 'tuic',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
udp: true,
...(0, index_1.pickAndFormatKeys)(nodeConfig, ['password', 'uuid', 'sni', 'skipCertVerify', 'version'], {
keyFormat: 'kebabCase',
}),
...(clashConfig.clashCore === 'stash' && nodeConfig.portHopping
? {
ports: nodeConfig.portHopping.replaceAll(';', ','),
}
: null),
...(clashConfig.clashCore === 'stash' &&
nodeConfig.portHoppingInterval
? {
'hop-interval': nodeConfig.portHoppingInterval,
}
: null),
...(nodeConfig.alpn ? { alpn: nodeConfig.alpn } : null),
};
}
return {
type: 'tuic',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
udp: true,
...(0, index_1.pickAndFormatKeys)(nodeConfig, ['token', 'sni', 'skipCertVerify', 'version'], {
keyFormat: 'kebabCase',
}),
...(clashConfig.clashCore === 'stash' && nodeConfig.portHopping
? {
ports: nodeConfig.portHopping.replaceAll(';', ','),
}
: null),
...(clashConfig.clashCore === 'stash' && nodeConfig.portHoppingInterval
? {
'hop-interval': nodeConfig.portHoppingInterval,
}
: null),
...(nodeConfig.alpn ? { alpn: nodeConfig.alpn } : null),
};
case types_1.NodeTypeEnum.Hysteria2:
// Istanbul ignore next
if (!clashConfig.enableHysteria2) {
logger.warn(`尚未开启 Clash 的 Hysteria2 支持,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableHysteria2 为 true。`);
return null;
}
return {
type: 'hysteria2',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
[clashConfig.clashCore === 'stash' ? 'auth' : 'password']: nodeConfig.password,
up: nodeConfig.uploadBandwidth || 0,
down: nodeConfig.downloadBandwidth || 0,
...(0, index_1.pickAndFormatKeys)(nodeConfig, ['obfs', 'obfsPassword', 'sni', 'skipCertVerify'], {
keyFormat: 'kebabCase',
}),
...((clashConfig.clashCore === 'stash' ||
clashConfig.clashCore === 'clash.meta') &&
nodeConfig.portHopping
? {
ports: nodeConfig.portHopping.replaceAll(';', ','),
}
: null),
...((clashConfig.clashCore === 'stash' ||
clashConfig.clashCore === 'clash.meta') &&
nodeConfig.portHoppingInterval
? {
'hop-interval': nodeConfig.portHoppingInterval,
}
: null),
...(nodeConfig.alpn ? { alpn: nodeConfig.alpn } : null),
};
case types_1.NodeTypeEnum.Wireguard:
// istanbul ignore next
if (nodeConfig.peers.length > 1) {
logger.warn(`节点 ${nodeConfig.nodeName} 有多个 WireGuard Peer,然而 Stash 或 Clash 仅支持一个 Peer,因此只会使用第一个 Peer。`);
}
return {
type: 'wireguard',
name: nodeConfig.nodeName,
'private-key': nodeConfig.privateKey,
ip: nodeConfig.selfIp,
...(nodeConfig.selfIpV6 ? { ipv6: nodeConfig.selfIpV6 } : null),
...(nodeConfig.mtu ? { mtu: nodeConfig.mtu } : null),
...(nodeConfig.dnsServers ? { dns: nodeConfig.dnsServers } : null),
udp: true,
// Peer
server: (0, index_1.getHostnameFromHost)(nodeConfig.peers[0].endpoint),
port: (0, index_1.getPortFromHost)(nodeConfig.peers[0].endpoint),
'public-key': nodeConfig.peers[0].publicKey,
...(nodeConfig.peers[0].presharedKey
? nodeConfig?.clashConfig?.clashCore === 'clash.meta'
? { 'pre-shared-key': nodeConfig.peers[0].presharedKey }
: { 'preshared-key': nodeConfig.peers[0].presharedKey }
: null),
...(nodeConfig.peers[0].reservedBits
? {
reserved: nodeConfig.peers[0].reservedBits,
}
: null),
};
// istanbul ignore next
default:
logger.warn(`不支持为 Clash 生成 ${nodeConfig.type} 的节点,节点 ${nodeConfig.nodeName} 会被省略`);
return null;
}
}
function resolveVmessHttpHeadersFromSurgioConfig(headers) {
return Object.keys(headers).reduce((acc, key) => {
acc[key] = [headers[key]];
return acc;
}, {});
}
//# sourceMappingURL=clash.js.map