UNPKG

@herbertgao/surgio

Version:

Generating rules for Surge, Clash, Quantumult like a PRO

377 lines 14.9 kB
"use strict"; 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