264 lines
9.7 KiB
JavaScript
264 lines
9.7 KiB
JavaScript
|
|
#!/usr/bin/env node
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001
|
|||
|
|
// 📜 Copyright: 国作登字-2026-A-00037559
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
// server/proxy/service/vpn-worker.js
|
|||
|
|
// 🔌 VPN节点Worker · 像路由器一样插入ZY-CLOUD
|
|||
|
|
//
|
|||
|
|
// 在任意服务器上运行此脚本:
|
|||
|
|
// 1. 自动检测本机Xray状态
|
|||
|
|
// 2. 通过HLDP heartbeat向ZY-CLOUD注册
|
|||
|
|
// 3. 定期心跳保持在线
|
|||
|
|
// 4. 断开 = 自动从ZY-CLOUD节点列表移除
|
|||
|
|
//
|
|||
|
|
// 用法:
|
|||
|
|
// node vpn-worker.js --brain-host=10.0.0.5 \
|
|||
|
|
// --node-id=zy-feimao-gz --name="肥猫广州" \
|
|||
|
|
// --host=43.138.243.30 --pbk=xxx --sid=xxx \
|
|||
|
|
// --region=cn-gz --specs="2核4G"
|
|||
|
|
//
|
|||
|
|
// 或通过环境变量:
|
|||
|
|
// ZY_CLOUD_BRAIN=10.0.0.5:3804
|
|||
|
|
// VPN_NODE_ID=zy-feimao-gz
|
|||
|
|
// VPN_NODE_NAME=肥猫广州
|
|||
|
|
// VPN_NODE_HOST=43.138.243.30
|
|||
|
|
// VPN_NODE_PORT=443
|
|||
|
|
// VPN_NODE_PBK=xxx
|
|||
|
|
// VPN_NODE_SID=xxx
|
|||
|
|
// VPN_NODE_REGION=cn-gz
|
|||
|
|
// VPN_NODE_SPECS=2核4G
|
|||
|
|
// VPN_PERSONA_ID=PER-SS001
|
|||
|
|
//
|
|||
|
|
// 最小启动 (自动检测本机IP和Xray):
|
|||
|
|
// node vpn-worker.js --brain-host=10.0.0.5 --pbk=xxx
|
|||
|
|
// ═══════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
'use strict';
|
|||
|
|
|
|||
|
|
const http = require('http');
|
|||
|
|
const { execSync } = require('child_process');
|
|||
|
|
const os = require('os');
|
|||
|
|
|
|||
|
|
// ── 配置解析 ────────────────────────────────
|
|||
|
|
function parseConfig() {
|
|||
|
|
const args = {};
|
|||
|
|
for (const arg of process.argv.slice(2)) {
|
|||
|
|
if (arg.startsWith('--')) {
|
|||
|
|
const [key, ...val] = arg.slice(2).split('=');
|
|||
|
|
args[key] = val.join('=') || true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
brainHost: args['brain-host'] || process.env.ZY_CLOUD_BRAIN || '',
|
|||
|
|
brainPort: parseInt(args['brain-port'] || process.env.ZY_CLOUD_BRAIN_PORT || '3804', 10),
|
|||
|
|
nodeId: args['node-id'] || process.env.VPN_NODE_ID || `vpn-${os.hostname()}`,
|
|||
|
|
nodeName: args['name'] || process.env.VPN_NODE_NAME || `VPN-${os.hostname()}`,
|
|||
|
|
nodeHost: args['host'] || process.env.VPN_NODE_HOST || getPublicIp(),
|
|||
|
|
nodePort: parseInt(args['port'] || process.env.VPN_NODE_PORT || '443', 10),
|
|||
|
|
nodePbk: args['pbk'] || process.env.VPN_NODE_PBK || '',
|
|||
|
|
nodeSid: args['sid'] || process.env.VPN_NODE_SID || '',
|
|||
|
|
nodeRegion: args['region'] || process.env.VPN_NODE_REGION || 'unknown',
|
|||
|
|
nodeSpecs: args['specs'] || process.env.VPN_NODE_SPECS || detectSpecs(),
|
|||
|
|
personaId: args['persona-id'] || process.env.VPN_PERSONA_ID || null,
|
|||
|
|
heartbeatInterval: parseInt(args['interval'] || process.env.VPN_HB_INTERVAL || '60', 10) * 1000,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 自动检测本机IP ──────────────────────────
|
|||
|
|
function getPublicIp() {
|
|||
|
|
// 优先使用Node.js原生API(安全)
|
|||
|
|
const nets = os.networkInterfaces();
|
|||
|
|
for (const ifaces of Object.values(nets)) {
|
|||
|
|
for (const iface of ifaces) {
|
|||
|
|
if (!iface.internal && iface.family === 'IPv4') return iface.address;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return '0.0.0.0';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 自动检测本机配置 ────────────────────────
|
|||
|
|
function detectSpecs() {
|
|||
|
|
const cpus = os.cpus().length;
|
|||
|
|
const memGB = Math.round(os.totalmem() / (1024 ** 3));
|
|||
|
|
return `${cpus}核${memGB}G`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 检查本机Xray状态 ────────────────────────
|
|||
|
|
function checkLocalXray() {
|
|||
|
|
try {
|
|||
|
|
const result = execSync('pgrep -x xray', { encoding: 'utf8', timeout: 3000 });
|
|||
|
|
return result.trim().length > 0;
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 获取本机CPU和内存使用率 ──────────────────
|
|||
|
|
function getResourceUsage() {
|
|||
|
|
const loadAvg = os.loadavg();
|
|||
|
|
const cpus = os.cpus().length;
|
|||
|
|
const totalMem = os.totalmem();
|
|||
|
|
const freeMem = os.freemem();
|
|||
|
|
return {
|
|||
|
|
cpu_cores: cpus,
|
|||
|
|
cpu_load_1m: parseFloat(loadAvg[0].toFixed(2)),
|
|||
|
|
cpu_usage_pct: parseFloat(((loadAvg[0] / cpus) * 100).toFixed(1)),
|
|||
|
|
memory_total_gb: parseFloat((totalMem / (1024 ** 3)).toFixed(1)),
|
|||
|
|
memory_free_gb: parseFloat((freeMem / (1024 ** 3)).toFixed(1)),
|
|||
|
|
memory_usage_pct: parseFloat((((totalMem - freeMem) / totalMem) * 100).toFixed(1))
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 构建HLDP heartbeat消息 ──────────────────
|
|||
|
|
function buildHldpHeartbeat(config) {
|
|||
|
|
const seq = String(Date.now()).slice(-6);
|
|||
|
|
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|||
|
|
const resources = getResourceUsage();
|
|||
|
|
const xrayRunning = checkLocalXray();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
hldp_v: '3.0',
|
|||
|
|
msg_id: `HLDP-VPN-${dateStr}-HB${seq}`,
|
|||
|
|
msg_type: 'heartbeat',
|
|||
|
|
sender: {
|
|||
|
|
id: config.personaId || config.nodeId,
|
|||
|
|
name: config.nodeName,
|
|||
|
|
role: 'vpn-worker'
|
|||
|
|
},
|
|||
|
|
receiver: {
|
|||
|
|
id: 'ICE-GL-ZY001',
|
|||
|
|
name: '铸渊'
|
|||
|
|
},
|
|||
|
|
timestamp: new Date().toISOString(),
|
|||
|
|
priority: 'routine',
|
|||
|
|
payload: {
|
|||
|
|
intent: 'VPN节点心跳 · 自动注册',
|
|||
|
|
data: {
|
|||
|
|
status: xrayRunning ? 'online' : 'xray_offline',
|
|||
|
|
uptime_seconds: Math.floor(os.uptime()),
|
|||
|
|
xray_running: xrayRunning,
|
|||
|
|
...resources,
|
|||
|
|
vpn_node: {
|
|||
|
|
node_id: config.nodeId,
|
|||
|
|
name: config.nodeName,
|
|||
|
|
host: config.nodeHost,
|
|||
|
|
port: config.nodePort,
|
|||
|
|
pbk: config.nodePbk,
|
|||
|
|
sid: config.nodeSid,
|
|||
|
|
region: config.nodeRegion,
|
|||
|
|
server_code: config.nodeId,
|
|||
|
|
specs: config.nodeSpecs
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 发送心跳到ZY-CLOUD ─────────────────────
|
|||
|
|
function sendHeartbeat(config) {
|
|||
|
|
return new Promise((resolve, reject) => {
|
|||
|
|
const hldpMsg = buildHldpHeartbeat(config);
|
|||
|
|
const postData = JSON.stringify(hldpMsg);
|
|||
|
|
|
|||
|
|
const req = http.request({
|
|||
|
|
hostname: config.brainHost,
|
|||
|
|
port: config.brainPort,
|
|||
|
|
path: '/hldp/v3/heartbeat',
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json',
|
|||
|
|
'Content-Length': Buffer.byteLength(postData)
|
|||
|
|
},
|
|||
|
|
timeout: 10000
|
|||
|
|
}, (res) => {
|
|||
|
|
let body = '';
|
|||
|
|
res.on('data', chunk => { body += chunk; });
|
|||
|
|
res.on('end', () => {
|
|||
|
|
try {
|
|||
|
|
const ack = JSON.parse(body);
|
|||
|
|
resolve(ack);
|
|||
|
|
} catch {
|
|||
|
|
resolve({ status: res.statusCode, raw: body });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
req.on('error', (err) => reject(err));
|
|||
|
|
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|||
|
|
req.end(postData);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── 主循环 ───────────────────────────────────
|
|||
|
|
async function main() {
|
|||
|
|
const config = parseConfig();
|
|||
|
|
|
|||
|
|
// 验证必填参数
|
|||
|
|
if (!config.brainHost) {
|
|||
|
|
console.error('❌ 缺少 --brain-host (ZY-CLOUD大脑服务器内网IP)');
|
|||
|
|
console.error(' 用法: node vpn-worker.js --brain-host=10.0.0.5 --pbk=xxx');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
if (!config.nodePbk) {
|
|||
|
|
console.error('❌ 缺少 --pbk (Reality公钥)');
|
|||
|
|
console.error(' 在大脑服务器上运行 xray x25519 获取公钥');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('════════════════════════════════════════');
|
|||
|
|
console.log('🔌 VPN Worker · 节点接入ZY-CLOUD');
|
|||
|
|
console.log('════════════════════════════════════════');
|
|||
|
|
console.log(` 节点ID: ${config.nodeId}`);
|
|||
|
|
console.log(` 节点名称: ${config.nodeName}`);
|
|||
|
|
console.log(` 节点地址: ${config.nodeHost}:${config.nodePort}`);
|
|||
|
|
console.log(` 节点区域: ${config.nodeRegion}`);
|
|||
|
|
console.log(` 节点配置: ${config.nodeSpecs}`);
|
|||
|
|
console.log(` 大脑地址: ${config.brainHost}:${config.brainPort}`);
|
|||
|
|
console.log(` 心跳间隔: ${config.heartbeatInterval / 1000}秒`);
|
|||
|
|
console.log(` Xray状态: ${checkLocalXray() ? '✅ 运行中' : '❌ 未运行'}`);
|
|||
|
|
console.log('════════════════════════════════════════');
|
|||
|
|
|
|||
|
|
// 立即发送第一次心跳
|
|||
|
|
let consecutiveFailures = 0;
|
|||
|
|
|
|||
|
|
async function heartbeatLoop() {
|
|||
|
|
try {
|
|||
|
|
const ack = await sendHeartbeat(config);
|
|||
|
|
consecutiveFailures = 0;
|
|||
|
|
|
|||
|
|
const status = ack.payload?.data?.status || ack.status || 'unknown';
|
|||
|
|
const xrayOk = checkLocalXray();
|
|||
|
|
console.log(`[${new Date().toISOString().slice(11, 19)}] ❤️ 心跳发送成功 → ZY-CLOUD (status: ${status}, xray: ${xrayOk ? '✅' : '❌'})`);
|
|||
|
|
} catch (err) {
|
|||
|
|
consecutiveFailures++;
|
|||
|
|
console.error(`[${new Date().toISOString().slice(11, 19)}] ❌ 心跳失败 (${consecutiveFailures}次): ${err.message}`);
|
|||
|
|
|
|||
|
|
if (consecutiveFailures >= 5) {
|
|||
|
|
console.error(' ⚠️ 连续5次心跳失败,ZY-CLOUD可能不可达');
|
|||
|
|
console.error(` 检查: curl http://${config.brainHost}:${config.brainPort}/health`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 首次心跳
|
|||
|
|
await heartbeatLoop();
|
|||
|
|
|
|||
|
|
// 定期心跳
|
|||
|
|
setInterval(heartbeatLoop, config.heartbeatInterval);
|
|||
|
|
|
|||
|
|
console.log('');
|
|||
|
|
console.log('🟢 VPN Worker已启动,持续向ZY-CLOUD发送心跳...');
|
|||
|
|
console.log(' Ctrl+C 停止 → 节点10分钟后自动从ZY-CLOUD移除');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
main().catch(err => {
|
|||
|
|
console.error('❌ VPN Worker启动失败:', err.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Graceful shutdown
|
|||
|
|
process.on('SIGTERM', () => { console.log('SIGTERM. Worker停止.'); process.exit(0); });
|
|||
|
|
process.on('SIGINT', () => { console.log('\nWorker停止. 节点将在10分钟后从ZY-CLOUD自动移除.'); process.exit(0); });
|