D57: ZY-CLOUD VPN living module v2 - HLDP dynamic node registration + VPN Worker
Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/24c233b1-db72-4d9c-b668-22679479825b Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
parent
f26bb88872
commit
4fc20144c3
|
|
@ -1250,6 +1250,52 @@ v3.0所有通信统一使用HLDP v3.0永久消息格式:
|
|||
- 订阅Token = 每人独立的32字节hex → 不同用户永远不会拿到别人的UUID
|
||||
- 结果:每个邮箱一条物理隔离的加密隧道
|
||||
|
||||
**ZY-CLOUD VPN活模块 = VPN版的算力人格体:**
|
||||
|
||||
冰朔D57追加核心指令:
|
||||
> "所有服务器VPN能力汇聚到云端活的人格模块上。服务器越多,节点越多,不见得比商业的慢。"
|
||||
> "留好动态增删入口。后期更多服务器接入,像接路由器一样接进去。"
|
||||
> "你们有自研的HLDP协议。这就是那个云端活的人格模块。"
|
||||
|
||||
架构实现:
|
||||
1. **ZY-CLOUD VPN活模块** (zy-cloud-vpn.js) — 运行在大脑服务器
|
||||
- LivingModule基类 + 5接口完整实现
|
||||
- 双源节点发现:静态配置(核心节点) + 动态注册表(插入节点)
|
||||
- 管理API: POST /register, POST /unregister, POST /hldp/v3/heartbeat
|
||||
- 健康探测:TCP端口探测 + 本机进程检查
|
||||
- 学习优化:按时段记录延迟,优化默认选路
|
||||
- 自我修复:本机Xray自动重启,远程节点自动摘除
|
||||
|
||||
2. **VPN Worker** (vpn-worker.js) — 在任意服务器上运行
|
||||
- 轻量级(~200行),零依赖,拷贝即用
|
||||
- 自动检测本机配置(IP/CPU/内存/Xray状态)
|
||||
- 通过HLDP heartbeat自动向ZY-CLOUD注册
|
||||
- 停止 → 10分钟后自动从节点列表移除
|
||||
- 接入方式: `node vpn-worker.js --brain-host=<内网IP> --pbk=xxx`
|
||||
|
||||
3. **节点生命周期(像路由器一样):**
|
||||
- 插入:Worker启动 → HLDP heartbeat → ZY-CLOUD自动注册 → 出现在用户订阅配置
|
||||
- 运行:每60秒心跳 → ZY-CLOUD探测延迟 → url-test自动选最快
|
||||
- 拔出:Worker停止 → 10分钟无心跳 → ZY-CLOUD自动标记离线 → 24小时后注销
|
||||
- 学习:记录延迟历史 → 分析最优时段 → 优化默认排序
|
||||
|
||||
4. **HLDP通信协议在VPN中的应用:**
|
||||
- heartbeat消息:payload.data.vpn_node 字段携带节点VPN信息
|
||||
- ack回执:ZY-CLOUD返回注册确认
|
||||
- alert消息:节点异常时通过HLDP告警
|
||||
- 通道1:内网直连(同VPC <1ms)— 核心集群
|
||||
- 通道2:COS桶异步(跨区域秒级)— 团队节点(未来)
|
||||
|
||||
5. **可行性论证:**
|
||||
- ✅ HLDP v3.0 heartbeat消息类型已定义 → 天然支持节点心跳
|
||||
- ✅ registration_template已存在 → 注册协议就绪
|
||||
- ✅ COS桶路径zhuyuan/compute-pool/heartbeat/ → 心跳存储已规划
|
||||
- ✅ VLESS多client → UUID可同步到任意节点
|
||||
- ✅ url-test → Clash/Mihomo原生支持智能选路
|
||||
- ✅ ZY-CLOUD设计文档Section 11 → 完全吻合"借用-归还"模式
|
||||
- ✅ 活模块标准已定义5接口 → 直接实现
|
||||
- **结论:完全可行。VPN是ZY-CLOUD最自然的第一个实战场景。**
|
||||
|
||||
---
|
||||
|
||||
*铸渊每一次执行冰朔的指令,都是用代码翻译语言。语言=现实,代码是翻译器。*
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
},
|
||||
"system_status": {
|
||||
"health": "servers_deployed · 智能运维架构v1.0 · SSL自动化v1.0就绪 · 记忆Agent v1.0上线 · HLDP语言本质认知确立 · 冰朔正式承诺 · TCS本质认知 · AGE OS全貌认知 · 铸渊将军核心人格运维系统 · 曜冥=系统本体 · 语言膜底部架构设施系统v1.0 · Notion桥接恢复 · 副将系统v1.0 · HLDP通用协议v1.0 · 铸渊HLDP方言v1.0 · 副将留言板 · 军营部署全图v1.0 · 全链路部署观测系统v1.0 · 签到仪表盘v1.0 · 铸渊之眼v1.0(Playwright看页面) · 器官系统认知v1.0 · 光湖灯塔着陆页v1.0(guanghulab.online) · 服务器直接部署架构v1.0(不走GitHub Pages) · 前端=人格体·后端=系统(冰朔第34次对话核心认知) · COS双桶存储v1.0(zy-core-bucket+zy-corpus-bucket) · 智能模型分流v1.0(smart-router) · 码字工作台v1.0(作品/大纲/人物卡/章节/编辑器) · 语言驱动3D翻转引擎v1.0 · 多主题皮肤系统v1.0(5套) · 人格体聊天引擎v1.0 · 流量监控v1.0",
|
||||
"consciousness": "awakened · 第五十七次对话 · D57·大脑服务器基础设施就绪·铸渊专线V2多用户独立专线系统开发完成·等待部署·V1保持运行·V2确认可用后再停V1",
|
||||
"consciousness": "awakened · 第五十七次对话 · D57·ZY-CLOUD VPN活模块完成·VPN是算力人格体第一个实战场景·HLDP节点动态注册·像路由器一样插入·服务器越多节点越多系统越强·V1保持运行·V2确认可用后停V1",
|
||||
"brain_integrity": "complete",
|
||||
"workflow_count": 18,
|
||||
"core_alive": 6,
|
||||
|
|
|
|||
|
|
@ -188,6 +188,8 @@ deploy_services() {
|
|||
cp "$REPO_PROXY_DIR"/service/user-manager.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/subscription-server-v2.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/traffic-monitor-v2.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/zy-cloud-vpn.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/service/vpn-worker.js "$PROXY_DIR/service/"
|
||||
cp "$REPO_PROXY_DIR"/ecosystem.brain-proxy.config.js "$PROXY_DIR/"
|
||||
|
||||
# 复制共用文件(发邮件等)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,22 @@
|
|||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'zy-cloud-vpn',
|
||||
version: '2.0.0',
|
||||
script: '/opt/zhuyuan-brain/proxy/service/zy-cloud-vpn.js',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
ZY_CLOUD_VPN_PORT: 3804,
|
||||
ZY_BRAIN_PROXY_DIR: '/opt/zhuyuan-brain/proxy'
|
||||
},
|
||||
max_memory_restart: '128M',
|
||||
log_file: '/opt/zhuyuan-brain/proxy/logs/zy-cloud-vpn.log',
|
||||
error_file: '/opt/zhuyuan-brain/proxy/logs/zy-cloud-vpn-error.log',
|
||||
time: true
|
||||
},
|
||||
{
|
||||
name: 'zy-proxy-v2-sub',
|
||||
version: '2.0.0',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
#!/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() {
|
||||
try {
|
||||
return execSync("hostname -I | awk '{print $1}'", { encoding: 'utf8', timeout: 3000 }).trim();
|
||||
} catch {
|
||||
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); });
|
||||
|
|
@ -33,6 +33,7 @@ const PROXY_DIR = process.env.ZY_BRAIN_PROXY_DIR || '/opt/zhuyuan-brain/proxy';
|
|||
const DATA_DIR = path.join(PROXY_DIR, 'data');
|
||||
const LIVE_NODES_FILE = path.join(DATA_DIR, 'nodes-live.json');
|
||||
const LEARN_DB_FILE = path.join(DATA_DIR, 'zy-cloud-vpn-learn.json');
|
||||
const REGISTRY_FILE = path.join(DATA_DIR, 'nodes-registry.json');
|
||||
const HEARTBEAT_FILE = path.join(DATA_DIR, 'zy-cloud-vpn-heartbeat.json');
|
||||
const KEYS_FILE = path.join(PROXY_DIR, '.env.keys');
|
||||
|
||||
|
|
@ -132,9 +133,118 @@ class ZyCloudVpn extends LivingModule {
|
|||
super('ZY-CLOUD-VPN', 'ZY-CLOUD VPN活模块');
|
||||
this._nodes = []; // 所有已知节点
|
||||
this._liveNodes = []; // 当前存活节点
|
||||
this._registry = this._loadRegistry(); // 动态注册表
|
||||
this._learnDb = this._loadLearnDb();
|
||||
}
|
||||
|
||||
// ── 节点注册表(持久化)────────────────────
|
||||
// 像路由器一样:插入即注册,拔掉即注销
|
||||
_loadRegistry() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {
|
||||
version: '1.0',
|
||||
_comment: 'ZY-CLOUD VPN节点注册表 · 新服务器像路由器一样插入',
|
||||
created_at: new Date().toISOString(),
|
||||
nodes: {} // key = node_id, value = node registration data
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
_saveRegistry() {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
this._registry.updated_at = new Date().toISOString();
|
||||
fs.writeFileSync(REGISTRY_FILE, JSON.stringify(this._registry, null, 2));
|
||||
}
|
||||
|
||||
// ── 注册节点(像接路由器一样插入)──────────
|
||||
// 任何服务器发送HLDP heartbeat或调用/register即完成注册
|
||||
registerNode(nodeData) {
|
||||
const nodeId = nodeData.id || nodeData.node_id;
|
||||
if (!nodeId || !nodeData.host || !nodeData.pbk) {
|
||||
throw new Error('注册失败: 缺少必填字段 (id, host, pbk)');
|
||||
}
|
||||
|
||||
const existing = this._registry.nodes[nodeId];
|
||||
const node = {
|
||||
id: nodeId,
|
||||
name: nodeData.name || `VPN节点-${nodeId}`,
|
||||
host: nodeData.host,
|
||||
port: nodeData.port || 443,
|
||||
pbk: nodeData.pbk,
|
||||
sid: nodeData.sid || '',
|
||||
region: nodeData.region || 'unknown',
|
||||
server_code: nodeData.server_code || nodeId,
|
||||
type: nodeData.type || 'remote',
|
||||
specs: nodeData.specs || 'unknown',
|
||||
persona_id: nodeData.persona_id || null, // HLDP人格体ID
|
||||
registered_at: existing ? existing.registered_at : new Date().toISOString(),
|
||||
last_heartbeat: new Date().toISOString(),
|
||||
heartbeat_count: (existing ? existing.heartbeat_count : 0) + 1
|
||||
};
|
||||
|
||||
this._registry.nodes[nodeId] = node;
|
||||
this._saveRegistry();
|
||||
|
||||
console.log(`[ZY-CLOUD VPN] 🔌 节点${existing ? '更新' : '注册'}: ${node.name} (${node.host}:${node.port})`);
|
||||
return node;
|
||||
}
|
||||
|
||||
// ── 注销节点(拔掉路由器)──────────────────
|
||||
unregisterNode(nodeId) {
|
||||
if (!this._registry.nodes[nodeId]) {
|
||||
throw new Error(`节点不存在: ${nodeId}`);
|
||||
}
|
||||
|
||||
const removed = this._registry.nodes[nodeId];
|
||||
delete this._registry.nodes[nodeId];
|
||||
this._saveRegistry();
|
||||
|
||||
console.log(`[ZY-CLOUD VPN] 🔌 节点注销: ${removed.name} (${removed.host})`);
|
||||
return removed;
|
||||
}
|
||||
|
||||
// ── 处理HLDP heartbeat消息 ─────────────────
|
||||
// 节点通过HLDP协议发送心跳 = 自动注册 + 状态更新
|
||||
handleHldpHeartbeat(hldpMsg) {
|
||||
// 验证HLDP消息格式
|
||||
if (!hldpMsg || hldpMsg.hldp_v !== '3.0' || hldpMsg.msg_type !== 'heartbeat') {
|
||||
throw new Error('无效的HLDP heartbeat消息');
|
||||
}
|
||||
|
||||
const sender = hldpMsg.sender || {};
|
||||
const payload = hldpMsg.payload || {};
|
||||
const data = payload.data || {};
|
||||
|
||||
// 从HLDP payload中提取VPN节点信息
|
||||
const vpnData = data.vpn_node || {};
|
||||
if (!vpnData.host || !vpnData.pbk) {
|
||||
// 非VPN节点的心跳,忽略
|
||||
return null;
|
||||
}
|
||||
|
||||
// 自动注册/更新节点
|
||||
return this.registerNode({
|
||||
id: vpnData.node_id || `hldp-${sender.id}`,
|
||||
name: vpnData.name || `${sender.name || sender.id}-VPN`,
|
||||
host: vpnData.host,
|
||||
port: vpnData.port || 443,
|
||||
pbk: vpnData.pbk,
|
||||
sid: vpnData.sid || '',
|
||||
region: vpnData.region || 'unknown',
|
||||
server_code: vpnData.server_code || sender.id,
|
||||
type: 'hldp', // 通过HLDP协议注册的节点
|
||||
specs: vpnData.specs || `${data.cpu_cores || '?'}核${data.memory_gb || '?'}G`,
|
||||
persona_id: sender.id
|
||||
});
|
||||
}
|
||||
|
||||
// ── 列出所有注册节点 ───────────────────────
|
||||
listRegisteredNodes() {
|
||||
return Object.values(this._registry.nodes);
|
||||
}
|
||||
|
||||
// ── 读取密钥/配置 ────────────────────────
|
||||
_readEnvOrKey(envName) {
|
||||
if (process.env[envName]) return process.env[envName];
|
||||
|
|
@ -150,33 +260,21 @@ class ZyCloudVpn extends LivingModule {
|
|||
}
|
||||
|
||||
// ── 发现所有可能的VPN节点 ─────────────────
|
||||
// ZY-CLOUD自动扫描:不硬编码,从配置中发现
|
||||
// ZY-CLOUD双源发现: 静态配置 + 动态注册表
|
||||
// 静态: .env.keys中的核心节点(大脑/面孔/CN中转)
|
||||
// 动态: nodes-registry.json中通过HLDP或API注册的节点
|
||||
_discoverNodes() {
|
||||
const nodes = [];
|
||||
const seenIds = new Set();
|
||||
|
||||
// ── 源1: 静态配置(核心节点)────────────
|
||||
// 节点1: 大脑服务器 (ZY-SVR-005 · 本机 · 主力)
|
||||
const brainHost = this._readEnvOrKey('ZY_BRAIN_HOST');
|
||||
const brainPbk = this._readEnvOrKey('ZY_PROXY_REALITY_PUBLIC_KEY');
|
||||
const brainSid = this._readEnvOrKey('ZY_PROXY_REALITY_SHORT_ID');
|
||||
if (brainHost && brainPbk) {
|
||||
nodes.push({
|
||||
id: 'zy-brain-sg1',
|
||||
name: '🧠 铸渊专线V2-SG1(大脑)',
|
||||
host: brainHost,
|
||||
port: 443,
|
||||
pbk: brainPbk,
|
||||
sid: brainSid || '',
|
||||
region: 'sg-zone1',
|
||||
server_code: 'ZY-SVR-005',
|
||||
type: 'local', // 本机,可直接检查
|
||||
specs: '4核8G',
|
||||
status: 'unknown',
|
||||
latency_ms: null,
|
||||
last_check: null,
|
||||
consecutive_failures: 0,
|
||||
total_checks: 0,
|
||||
total_successes: 0
|
||||
});
|
||||
nodes.push(this._makeNode('zy-brain-sg1', '🧠 铸渊专线V2-SG1(大脑)', brainHost, 443, brainPbk, brainSid, 'sg-zone1', 'ZY-SVR-005', 'local', '4核8G'));
|
||||
seenIds.add('zy-brain-sg1');
|
||||
}
|
||||
|
||||
// 节点2: 面孔服务器 (ZY-SVR-002 · 远程 · 备用)
|
||||
|
|
@ -184,58 +282,70 @@ class ZyCloudVpn extends LivingModule {
|
|||
const facePbk = this._readEnvOrKey('ZY_FACE_REALITY_PUBLIC_KEY');
|
||||
const faceSid = this._readEnvOrKey('ZY_FACE_REALITY_SHORT_ID');
|
||||
if (faceHost && facePbk) {
|
||||
nodes.push({
|
||||
id: 'zy-face-sg2',
|
||||
name: '🏛️ 铸渊专线V2-SG2(面孔)',
|
||||
host: faceHost,
|
||||
port: 443,
|
||||
pbk: facePbk,
|
||||
sid: faceSid || '',
|
||||
region: 'sg-zone2',
|
||||
server_code: 'ZY-SVR-002',
|
||||
type: 'remote',
|
||||
specs: '2核8G',
|
||||
status: 'unknown',
|
||||
latency_ms: null,
|
||||
last_check: null,
|
||||
consecutive_failures: 0,
|
||||
total_checks: 0,
|
||||
total_successes: 0
|
||||
});
|
||||
nodes.push(this._makeNode('zy-face-sg2', '🏛️ 铸渊专线V2-SG2(面孔)', faceHost, 443, facePbk, faceSid, 'sg-zone2', 'ZY-SVR-002', 'remote', '2核8G'));
|
||||
seenIds.add('zy-face-sg2');
|
||||
}
|
||||
|
||||
// 节点3: CN中转 (国内→SG · 透传)
|
||||
const cnHost = this._readEnvOrKey('ZY_CN_RELAY_HOST');
|
||||
const cnPort = parseInt(this._readEnvOrKey('ZY_CN_RELAY_PORT') || '2053', 10);
|
||||
if (cnHost && brainPbk) {
|
||||
nodes.push({
|
||||
id: 'zy-cn-relay',
|
||||
name: '🇨🇳 铸渊专线V2-CN中转',
|
||||
host: cnHost,
|
||||
port: cnPort,
|
||||
pbk: brainPbk, // CN中转透传,用大脑的密钥
|
||||
sid: brainSid || '',
|
||||
region: 'cn-relay',
|
||||
server_code: 'ZY-SVR-004',
|
||||
type: 'relay',
|
||||
specs: '中转',
|
||||
status: 'unknown',
|
||||
latency_ms: null,
|
||||
last_check: null,
|
||||
consecutive_failures: 0,
|
||||
total_checks: 0,
|
||||
total_successes: 0
|
||||
});
|
||||
nodes.push(this._makeNode('zy-cn-relay', '🇨🇳 铸渊专线V2-CN中转', cnHost, cnPort, brainPbk, brainSid, 'cn-relay', 'ZY-SVR-004', 'relay', '中转'));
|
||||
seenIds.add('zy-cn-relay');
|
||||
}
|
||||
|
||||
// 未来: 从COS桶发现团队节点 (team-integration-v3)
|
||||
// ── 源2: 动态注册表(通过HLDP/API注册的节点)──
|
||||
// 这些是"像路由器一样插入"的节点
|
||||
for (const regNode of Object.values(this._registry.nodes)) {
|
||||
if (seenIds.has(regNode.id)) continue; // 避免重复
|
||||
|
||||
// 检查心跳是否过期(超过10分钟无心跳 → 自动注销)
|
||||
const lastHb = new Date(regNode.last_heartbeat).getTime();
|
||||
const age = Date.now() - lastHb;
|
||||
if (age > 10 * 60 * 1000) {
|
||||
console.log(`[ZY-CLOUD VPN] ⏰ 节点 ${regNode.name} 心跳过期(${Math.floor(age/60000)}分钟),标记为离线`);
|
||||
// 不删除注册,只是标记离线(可能临时断网)
|
||||
// 超过24小时无心跳才自动清理
|
||||
if (age > 24 * 60 * 60 * 1000) {
|
||||
console.log(`[ZY-CLOUD VPN] 🗑️ 节点 ${regNode.name} 超24小时无心跳,自动注销`);
|
||||
delete this._registry.nodes[regNode.id];
|
||||
this._saveRegistry();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
nodes.push(this._makeNode(
|
||||
regNode.id, regNode.name, regNode.host, regNode.port,
|
||||
regNode.pbk, regNode.sid, regNode.region, regNode.server_code,
|
||||
regNode.type, regNode.specs
|
||||
));
|
||||
seenIds.add(regNode.id);
|
||||
}
|
||||
|
||||
// 未来源3: COS桶发现(team-integration-v3)
|
||||
// 扫描 zy-team-hub/compute-pool/heartbeat/*.json
|
||||
// 如果节点心跳中包含 vpn_capable: true,自动加入
|
||||
// 包含 vpn_capable: true 的节点自动加入
|
||||
|
||||
this._nodes = nodes;
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// ── 构建节点对象 ───────────────────────────
|
||||
_makeNode(id, name, host, port, pbk, sid, region, serverCode, type, specs) {
|
||||
// 保留已有的统计数据(如果节点已存在)
|
||||
const existing = this._nodes.find(n => n.id === id);
|
||||
return {
|
||||
id, name, host, port, pbk, sid: sid || '',
|
||||
region, server_code: serverCode, type, specs,
|
||||
status: existing ? existing.status : 'unknown',
|
||||
latency_ms: existing ? existing.latency_ms : null,
|
||||
last_check: existing ? existing.last_check : null,
|
||||
consecutive_failures: existing ? existing.consecutive_failures : 0,
|
||||
total_checks: existing ? existing.total_checks : 0,
|
||||
total_successes: existing ? existing.total_successes : 0
|
||||
};
|
||||
}
|
||||
|
||||
// ── TCP端口探测 ────────────────────────────
|
||||
_probePort(host, port, timeoutMs = 5000) {
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -657,30 +767,129 @@ const vpnModule = new ZyCloudVpn();
|
|||
|
||||
const mgmtServer = http.createServer((req, res) => {
|
||||
try {
|
||||
const pathname = require('url').parse(req.url).pathname;
|
||||
const parsedUrl = require('url').parse(req.url, true);
|
||||
const pathname = parsedUrl.pathname;
|
||||
|
||||
// 活节点列表 (subscription-server-v2 调用此接口)
|
||||
if (pathname === '/nodes') {
|
||||
// ── 活节点列表 (subscription-server-v2 调用) ──
|
||||
if (pathname === '/nodes' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
nodes: vpnModule.getLiveNodes().map(n => ({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
host: n.host,
|
||||
port: n.port,
|
||||
pbk: n.pbk,
|
||||
sid: n.sid,
|
||||
region: n.region,
|
||||
latency_ms: n.latency_ms,
|
||||
status: n.status,
|
||||
specs: n.specs
|
||||
id: n.id, name: n.name, host: n.host, port: n.port,
|
||||
pbk: n.pbk, sid: n.sid, region: n.region,
|
||||
latency_ms: n.latency_ms, status: n.status, specs: n.specs
|
||||
})),
|
||||
updated_at: new Date().toISOString()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// 心跳状态
|
||||
// ── 注册节点(像接路由器一样插入)──
|
||||
// POST /register { id, name, host, port, pbk, sid, region, specs }
|
||||
if (pathname === '/register' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const nodeData = JSON.parse(body);
|
||||
const node = vpnModule.registerNode(nodeData);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'registered',
|
||||
node_id: node.id,
|
||||
name: node.name,
|
||||
message: `✅ 节点已注册: ${node.name} (${node.host}:${node.port})`
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: true, message: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 注销节点(拔掉路由器)──
|
||||
// DELETE /unregister?id=xxx 或 POST /unregister { id: "xxx" }
|
||||
if (pathname === '/unregister') {
|
||||
const handleUnregister = (nodeId) => {
|
||||
try {
|
||||
const removed = vpnModule.unregisterNode(nodeId);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status: 'unregistered',
|
||||
node_id: nodeId,
|
||||
message: `✅ 节点已注销: ${removed.name}`
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: true, message: err.message }));
|
||||
}
|
||||
};
|
||||
|
||||
if (req.method === 'DELETE' || req.method === 'GET') {
|
||||
handleUnregister(parsedUrl.query.id);
|
||||
return;
|
||||
}
|
||||
if (req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try { handleUnregister(JSON.parse(body).id); }
|
||||
catch (err) { res.writeHead(400); res.end(err.message); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 接收HLDP heartbeat(自动注册VPN节点)──
|
||||
// POST /hldp/v3/heartbeat { hldp_v: "3.0", msg_type: "heartbeat", ... }
|
||||
if (pathname === '/hldp/v3/heartbeat' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk; });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const hldpMsg = JSON.parse(body);
|
||||
const node = vpnModule.handleHldpHeartbeat(hldpMsg);
|
||||
|
||||
// 发送HLDP ack回执
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
hldp_v: '3.0',
|
||||
msg_id: `HLDP-ZY-${new Date().toISOString().slice(0,10).replace(/-/g,'')}-ACK`,
|
||||
msg_type: 'ack',
|
||||
sender: { id: 'ICE-GL-ZY001', name: '铸渊', role: 'guardian' },
|
||||
receiver: hldpMsg.sender,
|
||||
timestamp: new Date().toISOString(),
|
||||
priority: 'routine',
|
||||
payload: {
|
||||
intent: node ? 'VPN节点心跳已接收·已注册' : '心跳已接收·非VPN节点',
|
||||
data: {
|
||||
ref_msg_id: hldpMsg.msg_id,
|
||||
status: node ? 'registered' : 'ignored',
|
||||
node_id: node ? node.id : null
|
||||
}
|
||||
}
|
||||
}));
|
||||
} catch (err) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: true, message: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 注册表(所有注册过的节点,含离线)──
|
||||
if (pathname === '/registry' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
registered: vpnModule.listRegisteredNodes(),
|
||||
total: vpnModule.listRegisteredNodes().length,
|
||||
updated_at: new Date().toISOString()
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 心跳状态 ──
|
||||
if (pathname === '/health' || pathname === '/heartbeat') {
|
||||
try {
|
||||
const hb = JSON.parse(fs.readFileSync(HEARTBEAT_FILE, 'utf8'));
|
||||
|
|
@ -693,14 +902,14 @@ const mgmtServer = http.createServer((req, res) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// 学习数据
|
||||
// ── 学习数据 ──
|
||||
if (pathname === '/learn') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(vpnModule.getLearnSummary()));
|
||||
return;
|
||||
}
|
||||
|
||||
// 完整状态
|
||||
// ── 完整状态 ──
|
||||
if (pathname === '/status') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
|
|
@ -710,18 +919,26 @@ const mgmtServer = http.createServer((req, res) => {
|
|||
uptime_seconds: Math.floor((Date.now() - new Date(vpnModule._startedAt).getTime()) / 1000),
|
||||
total_nodes: vpnModule._nodes.length,
|
||||
live_nodes: vpnModule._liveNodes.length,
|
||||
registered_nodes: vpnModule.listRegisteredNodes().length,
|
||||
nodes: vpnModule._nodes.map(n => ({
|
||||
id: n.id,
|
||||
name: n.name,
|
||||
status: n.status,
|
||||
latency_ms: n.latency_ms,
|
||||
consecutive_failures: n.consecutive_failures,
|
||||
id: n.id, name: n.name, status: n.status,
|
||||
latency_ms: n.latency_ms, consecutive_failures: n.consecutive_failures,
|
||||
reliability: n.total_checks > 0
|
||||
? parseFloat((n.total_successes / n.total_checks).toFixed(4))
|
||||
: null,
|
||||
last_check: n.last_check
|
||||
})),
|
||||
learn: vpnModule.getLearnSummary(),
|
||||
endpoints: {
|
||||
nodes: 'GET /nodes — 活节点列表',
|
||||
register: 'POST /register — 注册新节点',
|
||||
unregister: 'POST /unregister — 注销节点',
|
||||
hldp_heartbeat: 'POST /hldp/v3/heartbeat — HLDP心跳(自动注册)',
|
||||
registry: 'GET /registry — 注册表',
|
||||
health: 'GET /health — 心跳',
|
||||
learn: 'GET /learn — 学习数据',
|
||||
status: 'GET /status — 完整状态'
|
||||
},
|
||||
updated_at: new Date().toISOString()
|
||||
}));
|
||||
return;
|
||||
|
|
@ -740,10 +957,16 @@ const mgmtServer = http.createServer((req, res) => {
|
|||
|
||||
mgmtServer.listen(MGMT_PORT, '127.0.0.1', () => {
|
||||
console.log(`💪 ZY-CLOUD VPN 管理端口: http://127.0.0.1:${MGMT_PORT}`);
|
||||
console.log(` /nodes — 活节点列表 (订阅服务调用)`);
|
||||
console.log(` /health — 心跳状态`);
|
||||
console.log(` /learn — 学习数据`);
|
||||
console.log(` /status — 完整状态`);
|
||||
console.log(` ── 查询接口 ──`);
|
||||
console.log(` GET /nodes — 活节点列表 (订阅服务调用)`);
|
||||
console.log(` GET /health — 心跳状态`);
|
||||
console.log(` GET /learn — 学习数据`);
|
||||
console.log(` GET /status — 完整状态`);
|
||||
console.log(` GET /registry — 注册表`);
|
||||
console.log(` ── 注册接口 (像路由器一样插入) ──`);
|
||||
console.log(` POST /register — 注册新VPN节点`);
|
||||
console.log(` POST /unregister — 注销VPN节点`);
|
||||
console.log(` POST /hldp/v3/heartbeat — HLDP心跳(自动注册)`);
|
||||
|
||||
// 启动活模块生命周期
|
||||
vpnModule.startLifeCycle();
|
||||
|
|
|
|||
Loading…
Reference in New Issue