@herbertgao/surgio
Version:
Generating rules for Surge, Clash, Quantumult like a PRO
377 lines • 14.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSingboxNodeNames = exports.getSingboxNodes = void 0;
const logger_1 = require("@surgio/logger");
const constant_1 = require("../constant");
const types_1 = require("../types");
const filters_1 = require("../filters");
const validators_1 = require("../validators");
const ss_1 = require("./ss");
const _1 = require("./");
const logger = (0, logger_1.createLogger)({ service: 'surgio:utils:singbox' });
const getSingboxNodes = function (list, filter) {
return (0, filters_1.applyFilter)(list, filter)
.flatMap(nodeListMapper)
.filter((item) => (0, _1.checkNotNullish)(item));
};
exports.getSingboxNodes = getSingboxNodes;
const getSingboxNodeNames = function (list, filter) {
// istanbul ignore next
if (arguments.length === 2 && typeof filter === 'undefined') {
throw new Error(constant_1.ERR_INVALID_FILTER);
}
return (0, exports.getSingboxNodes)(list, filter).map((item) => item.tag);
};
exports.getSingboxNodeNames = getSingboxNodeNames;
const typeMap = {
[types_1.NodeTypeEnum.HTTP]: 'http',
[types_1.NodeTypeEnum.HTTPS]: 'http',
[types_1.NodeTypeEnum.Shadowsocks]: 'shadowsocks',
[types_1.NodeTypeEnum.Vmess]: 'vmess',
[types_1.NodeTypeEnum.Vless]: 'vless',
[types_1.NodeTypeEnum.Trojan]: 'trojan',
[types_1.NodeTypeEnum.Socks5]: 'socks',
[types_1.NodeTypeEnum.Tuic]: 'tuic',
[types_1.NodeTypeEnum.Wireguard]: 'wireguard',
[types_1.NodeTypeEnum.Hysteria2]: 'hysteria2',
};
/**
* @see https://sing-box.sagernet.org/configuration/outbound/
*/
function nodeListMapper(nodeConfig) {
if (nodeConfig.type in typeMap === false) {
logger.warn(`不支持为 sing-box 生成 ${nodeConfig.type} 的节点,节点 ${nodeConfig.nodeName} 会被忽略`);
return null;
}
const node = {
type: typeMap[nodeConfig.type],
tag: nodeConfig.nodeName,
};
if ('hostname' in nodeConfig) {
node.server = nodeConfig.hostname;
}
if ('port' in nodeConfig) {
node.server_port = Number(nodeConfig.port);
}
if ('udpRelay' in nodeConfig && nodeConfig.udpRelay === false) {
node.network = 'tcp';
}
const setTls = (field, v) => {
if (!node.tls) {
node.tls = { enabled: true };
}
node.tls[field] = v;
};
switch (nodeConfig.type) {
case types_1.NodeTypeEnum.Shadowsocks:
node.method = nodeConfig.method;
node.password = nodeConfig.password;
if (nodeConfig.obfs) {
if (['tls', 'http'].includes(nodeConfig.obfs)) {
node.plugin = 'obfs-local';
node.plugin_opts = (0, ss_1.stringifySip003Options)(prune({
obfs: nodeConfig.obfs,
'obfs-host': nodeConfig.obfsHost,
}));
}
if (['ws', 'wss', 'quic'].includes(nodeConfig.obfs)) {
node.plugin = 'v2ray-plugin';
node.plugin_opts = (0, ss_1.stringifySip003Options)(prune({
mode: nodeConfig.obfs === 'quic' ? 'quic' : 'websocket',
tls: ['wss', 'quic'].includes(nodeConfig.obfs),
host: nodeConfig.obfsHost,
path: nodeConfig.obfsUri,
mux: nodeConfig.mux === false ? 0 : null,
}));
}
}
break;
case types_1.NodeTypeEnum.Vless:
case types_1.NodeTypeEnum.Vmess: {
node.uuid = nodeConfig.uuid;
if (nodeConfig.type === types_1.NodeTypeEnum.Vmess) {
node.security = nodeConfig.method;
if (nodeConfig.alterId) {
node.alter_id = Number(nodeConfig.alterId);
}
}
if (nodeConfig.type === types_1.NodeTypeEnum.Vless) {
node.flow = nodeConfig.flow;
if (nodeConfig.realityOpts) {
setTls('utls', {
enabled: true,
fingerprint: nodeConfig.clientFingerprint,
});
setTls('reality', {
enabled: true,
public_key: nodeConfig.realityOpts.publicKey,
short_id: nodeConfig.realityOpts.shortId,
});
}
}
switch (nodeConfig.network) {
case 'http':
node.transport = {
type: 'http',
// host: [],
path: nodeConfig.httpOpts?.path[0],
method: nodeConfig.httpOpts?.method,
headers: normalizeHeaders(nodeConfig.httpOpts?.headers),
// idle_timeout: '15s',
// ping_timeout: '15s',
};
break;
case 'ws':
node.transport = {
type: 'ws',
path: nodeConfig.wsOpts?.path,
headers: normalizeHeaders(nodeConfig.wsOpts?.headers),
// max_early_data: 0,
// early_data_header_name: '',
};
break;
case 'quic':
node.transport = {
type: 'quic',
};
break;
case 'grpc':
node.transport = {
type: 'grpc',
service_name: nodeConfig.grpcOpts?.serviceName,
// idle_timeout: '15s',
// ping_timeout: '15s',
// permit_without_stream: false,
};
break;
case 'httpupgrade':
node.transport = {
type: 'httpupgrade',
host: nodeConfig.httpUpgradeOpts?.host,
path: nodeConfig.httpUpgradeOpts?.path,
headers: normalizeHeaders(nodeConfig.httpUpgradeOpts?.headers),
};
break;
case 'tcp':
break;
default:
logger.warn(`sing-box 的 ${nodeConfig.type} 节点不支持 network=${nodeConfig.network},节点 ${nodeConfig.nodeName} 会被忽略`);
return null;
}
break;
}
case types_1.NodeTypeEnum.HTTP:
case types_1.NodeTypeEnum.HTTPS:
node.username = nodeConfig.username;
node.password = nodeConfig.password;
node.path = nodeConfig.path;
node.headers = normalizeHeaders(nodeConfig.headers);
if (nodeConfig.type === types_1.NodeTypeEnum.HTTPS) {
setTls('enabled', true);
}
break;
case types_1.NodeTypeEnum.Trojan:
node.password = nodeConfig.password;
if (nodeConfig.network) {
switch (nodeConfig.network) {
case 'ws':
node.transport = {
type: 'ws',
path: nodeConfig.wsPath,
headers: normalizeHeaders(nodeConfig.wsHeaders),
// max_early_data: 0,
// early_data_header_name: '',
};
break;
default:
logger.warn(`sing-box 的 ${nodeConfig.type} 节点不支持 network=${nodeConfig.network},节点 ${nodeConfig.nodeName} 会被忽略`);
return null;
}
}
break;
case types_1.NodeTypeEnum.Socks5:
node.username = nodeConfig.username;
node.password = nodeConfig.password;
break;
case types_1.NodeTypeEnum.Tuic:
if ('uuid' in nodeConfig === false) {
logger.warn(`sing-box 仅支持 tuic v5,节点 ${nodeConfig.nodeName} 会被忽略`);
return null;
}
node.uuid = nodeConfig.uuid;
node.password = nodeConfig.password;
// congestion_control: 'cubic',
// udp_relay_mode: 'native',
// udp_over_stream: false,
// zero_rtt_handshake: false,
// heartbeat: '10s',
break;
case types_1.NodeTypeEnum.Hysteria2:
node.up_mbps = nodeConfig.uploadBandwidth;
node.down_mbps = nodeConfig.downloadBandwidth;
node.obfs = {
type: nodeConfig.obfs,
password: nodeConfig.obfsPassword,
};
node.password = nodeConfig.password;
if (nodeConfig.portHopping) {
const ports = nodeConfig.portHopping
.split(',')
.filter((portConfig) => portConfig.includes('-'))
.map((portConfig) => portConfig.replace(/-/g, ':'));
node.server_ports = ports;
}
if (nodeConfig.portHoppingInterval) {
node.hop_interval = `${nodeConfig.portHoppingInterval}s`;
}
break;
case types_1.NodeTypeEnum.Wireguard:
// const sample = {
// system_interface: false,
// gso: false,
// interface_name: 'wg0',
// address: ['10.0.0.2/32'],
// private_key: 'YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=',
// peers: [
// {
// address: '127.0.0.1',
// port: 1080,
// public_key: 'Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=',
// pre_shared_key: '31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=',
// allowed_ips: ['0.0.0.0/0'],
// reserved: [0, 0, 0],
// },
// ],
// peer_public_key: 'Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=',
// pre_shared_key: '31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=',
// reserved: [0, 0, 0],
// workers: 4,
// mtu: 1408,
// }
node.address = [`${nodeConfig.selfIp}/32`];
if (nodeConfig.selfIpV6) {
node.address.push(`${nodeConfig.selfIpV6}/128`);
}
node.private_key = nodeConfig.privateKey;
node.peers = nodeConfig.peers.map((peer) => {
const endpoint = new URL(`http://${peer.endpoint}`);
return {
address: endpoint.hostname,
port: Number(endpoint.port),
public_key: peer.publicKey,
pre_shared_key: peer.presharedKey,
allowed_ips: peer.allowedIps?.split(',').map((ip) => ip.trim()),
reserved: peer.reservedBits,
};
});
node.mtu = nodeConfig.mtu;
break;
}
if ('tls' in nodeConfig && nodeConfig.tls) {
setTls('enabled', true);
}
const r = validators_1.TlsNodeConfigValidator.safeParse(nodeConfig);
if (r.success) {
const tlsConfig = r.data;
if (tlsConfig.sni) {
setTls('server_name', tlsConfig.sni);
}
if (tlsConfig.skipCertVerify) {
setTls('insecure', true);
}
if (tlsConfig.alpn) {
setTls('alpn', tlsConfig.alpn);
}
if (tlsConfig.tls13) {
setTls('min_version', '1.3');
}
if (tlsConfig.clientFingerprint) {
setTls('utls', {
enabled: true,
fingerprint: tlsConfig.clientFingerprint,
});
}
}
if ('multiplex' in nodeConfig) {
const r = validators_1.MultiplexValidator.safeParse(nodeConfig.multiplex);
if (r.success) {
const multiplexConfig = r.data;
node.multiplex = (0, _1.pickAndFormatKeys)(multiplexConfig, ['protocol', 'maxConnections', 'minStreams', 'maxStreams', 'padding'], { keyFormat: 'snakeCase' });
node.multiplex.enabled = true;
if (multiplexConfig.brutal) {
node.multiplex.brutal = (0, _1.pickAndFormatKeys)(multiplexConfig.brutal, ['upMbps', 'downMbps'], { keyFormat: 'snakeCase' });
}
}
}
if (nodeConfig.tfo) {
node.tcp_fast_open = true;
}
if (nodeConfig.mptcp) {
node.tcp_multi_path = true;
}
if (nodeConfig.underlyingProxy) {
node.detour = nodeConfig.underlyingProxy;
}
if (!nodeConfig.shadowTls) {
return prune(node);
}
const { server, server_port, ..._node } = node;
const tag = `${node.tag}-shadowtls`;
_node.detour = tag;
return [
prune(_node),
prune({
type: 'shadowtls',
tag: tag,
server: server,
server_port: server_port,
version: nodeConfig.shadowTls.version,
password: nodeConfig.shadowTls.password,
tls: {
enabled: true,
server_name: nodeConfig.shadowTls.sni,
},
}),
];
}
function normalizeHeaders(headers) {
if (!headers) {
return {};
}
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k, [v]]));
}
// delete all undefined / null / [] / {} / '' properties
function prune(obj) {
const prunedObj = {};
for (const key in obj) {
const value = obj[key];
// Check if the property exists and is not null or undefined
if (value !== null && value !== undefined) {
// Check if the property is an array
if (Array.isArray(value)) {
// Check if the array is empty
if (value.length > 0) {
prunedObj[key] = value;
}
}
else if (typeof value === 'object') {
// Check if the object is empty
if (Object.keys(value).length > 0) {
prunedObj[key] = prune(value); // Recursively prune the object
}
}
else if (typeof value === 'string') {
// Check if the string is not empty
if (value.trim().length > 0) {
prunedObj[key] = value;
}
}
else {
// Add non-empty string, number, or boolean values
prunedObj[key] = value;
}
}
}
return prunedObj;
}
//# sourceMappingURL=singbox.js.map