Files
clawd/lib/headscale.js
2026-05-04 09:46:05 +08:00

95 lines
2.9 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.
'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 };