'use strict'; const { execSync, exec } = require('child_process'); const fs = require('fs'); const log = require('./logger'); const TAILSCALE_BIN_CANDIDATES = [ '/usr/bin/tailscale', '/usr/local/bin/tailscale', '/sbin/tailscale', ]; /** 查找 tailscale 可执行文件路径,找不到返回 null */ function findTailscaleBin() { for (const p of TAILSCALE_BIN_CANDIDATES) { if (fs.existsSync(p)) return p; } return null; } /** tailscale 是否已安装 */ function isInstalled() { return findTailscaleBin() !== null; } /** * 检查是否已加入指定 headscale 服务端。 * @param {string} loginServer headscale 服务地址,如 https://hs.claw.cutos.ai * @returns {boolean} */ function isJoined(loginServer) { const bin = findTailscaleBin(); if (!bin) return false; try { const out = execSync(`${bin} status --json`, { timeout: 5000 }).toString(); const status = JSON.parse(out); if (status.BackendState !== 'Running') return false; const currentServer = (status.CurrentTailnet?.MagicDNSSuffix || '') || status.ControlURL || ''; // 检查是否连接到同一 headscale 实例(比对 hostname) const serverHost = new URL(loginServer).hostname; return currentServer.includes(serverHost) || (status.Self?.Online === true); } catch (_) { return false; } } /** * 加入 headscale mesh 网络。 * @param {string} loginServer headscale 服务地址 * @param {string} authkey 一次性 preauth key * @param {string} hostname 节点 hostname(如 claw-1014) * @returns {Promise} 是否成功 */ function join(loginServer, authkey, hostname) { return new Promise((resolve) => { const bin = findTailscaleBin(); if (!bin) { log.warn('headscale', 'tailscale 未安装,跳过 mesh 注册'); resolve(false); return; } const cmd = `${bin} up --login-server "${loginServer}" --authkey "${authkey}" --hostname "${hostname}" --accept-routes`; log.info('headscale', `加入 mesh: ${loginServer}, hostname=${hostname}`); exec(cmd, { timeout: 30_000 }, (err, stdout, stderr) => { if (err) { log.error('headscale', `tailscale up 失败: ${stderr || err.message}`); resolve(false); } else { log.info('headscale', `成功加入 mesh: ${stdout.trim() || 'ok'}`); resolve(true); } }); }); } /** * 退出 headscale mesh 网络(解绑时调用)。 * @returns {Promise} */ function logout() { return new Promise((resolve) => { const bin = findTailscaleBin(); if (!bin) { resolve(); return; } exec(`${bin} logout`, { timeout: 10_000 }, (err) => { if (err) log.warn('headscale', `tailscale logout 失败: ${err.message}`); else log.info('headscale', 'tailscale logout 成功'); resolve(); }); }); } module.exports = { isInstalled, isJoined, join, logout };