95 lines
2.9 KiB
JavaScript
95 lines
2.9 KiB
JavaScript
'use strict';
|
||
|
||
const { execSync, exec } = require('child_process');
|
||
const fs = require('fs');
|
||
const log = require('./logger');
|
||
|
||
const TAILSCALE_BIN_CANDIDATES = [
|
||
'/etc/clawd/tailscale/tailscale',
|
||
'/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<boolean>} 是否成功
|
||
*/
|
||
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<void>}
|
||
*/
|
||
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 };
|