guanghulab/server/proxy/service/vpn-worker.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

264 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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); });