feat: SSH STCP key generation and frp tunnel registration

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
stswangzhiping
2026-05-02 18:07:33 +08:00
parent 7fe1ee64d8
commit 1a7e0a9738
3 changed files with 36 additions and 14 deletions

View File

@@ -375,6 +375,7 @@ class ClawClient {
claw_id: this._cfg.claw_id ?? null, claw_id: this._cfg.claw_id ?? null,
token: this._cfg.token ?? null, token: this._cfg.token ?? null,
version: CLAWD_VERSION, version: CLAWD_VERSION,
ssh_secret_key: this._cfg.ssh_secret_key ?? null,
local_ip: getLocalIps(), local_ip: getLocalIps(),
local_networks: getLocalNetworks(), local_networks: getLocalNetworks(),
external_ip: this._externalIp ?? null, external_ip: this._externalIp ?? null,
@@ -437,7 +438,7 @@ class ClawClient {
this._applyStatus(msg); this._applyStatus(msg);
if (msg.frp && msg.frp.server && msg.frp.auth_token) { if (msg.frp && msg.frp.server && msg.frp.auth_token) {
this._frpc.start(msg.claw_id, msg.frp).catch(e => { this._frpc.start(msg.claw_id, msg.frp, this._cfg.ssh_secret_key ?? null).catch(e => {
log.error('frpc', '启动失败:', e.message); log.error('frpc', '启动失败:', e.message);
}); });
} }

View File

@@ -19,19 +19,33 @@ const DEFAULTS = {
heartbeat_interval: 30, // 秒 heartbeat_interval: 30, // 秒
/** 云端已激活:用于启动/重连时立即点亮 alarmpwr不等首包 connected */ /** 云端已激活:用于启动/重连时立即点亮 alarmpwr不等首包 connected */
activated: false, activated: false,
ssh_secret_key: null,
}; };
function _generateSshSecretKey() {
const bytes = require('crypto').randomBytes(16);
return 'sk-' + bytes.toString('hex');
}
function load() { function load() {
let cfg;
try { try {
if (fs.existsSync(CONFIG_FILE)) { if (fs.existsSync(CONFIG_FILE)) {
const raw = fs.readFileSync(CONFIG_FILE, 'utf8'); const raw = fs.readFileSync(CONFIG_FILE, 'utf8');
return Object.assign({}, DEFAULTS, JSON.parse(raw)); cfg = Object.assign({}, DEFAULTS, JSON.parse(raw));
} }
} catch (e) { } catch (e) {
const log = require('./logger'); const log = require('./logger');
log.error('config', '读取配置失败,使用默认值:', e.message); log.error('config', '读取配置失败,使用默认值:', e.message);
} }
return Object.assign({}, DEFAULTS); if (!cfg) cfg = Object.assign({}, DEFAULTS);
if (!cfg.ssh_secret_key) {
cfg.ssh_secret_key = _generateSshSecretKey();
save(cfg);
}
return cfg;
} }
function save(data) { function save(data) {

View File

@@ -143,9 +143,16 @@ function downloadFile(url, dest) {
}); });
} }
function writeFrpcConfig(clawId, frpConfig) { function writeFrpcConfig(clawId, frpConfig, sshSecretKey) {
const { auth_token, dashboard_local_port = 18789 } = frpConfig; const { auth_token, dashboard_local_port = 18789 } = frpConfig;
const ttyRemotePort = 10000 + Number(clawId); const ttyRemotePort = 10000 + Number(clawId);
const stcpBlock = sshSecretKey ? `
[[proxies]]
name = "ssh-${clawId}-secret"
type = "stcp"
secretKey = "${sshSecretKey}"
localPort = 22
` : '';
const toml = `# 由 clawd 自动生成,请勿手动修改 const toml = `# 由 clawd 自动生成,请勿手动修改
serverAddr = "frp.claw.cutos.ai" serverAddr = "frp.claw.cutos.ai"
serverPort = 443 serverPort = 443
@@ -168,10 +175,10 @@ name = "tty-${clawId}"
type = "tcp" type = "tcp"
localPort = ${TTYD_PORT} localPort = ${TTYD_PORT}
remotePort = ${ttyRemotePort} remotePort = ${ttyRemotePort}
`; ${stcpBlock}`;
fs.mkdirSync(CONFIG_DIR, { recursive: true }); fs.mkdirSync(CONFIG_DIR, { recursive: true });
fs.writeFileSync(FRPC_CONFIG, toml, 'utf8'); fs.writeFileSync(FRPC_CONFIG, toml, 'utf8');
log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}`); log.info('frpc', `frpc.toml 已写入: dashboard subdomain=${clawId}, tty tcp-port=${ttyRemotePort}${sshSecretKey ? ', ssh stcp=enabled' : ''}`);
} }
/** /**
@@ -183,7 +190,7 @@ class FrpcManager {
this._watchdog = null; this._watchdog = null;
} }
async start(clawId, frpConfig) { async start(clawId, frpConfig, sshSecretKey) {
this.stop(); this.stop();
if (!fs.existsSync(FRPC_BIN)) { if (!fs.existsSync(FRPC_BIN)) {
@@ -195,7 +202,7 @@ class FrpcManager {
} }
} }
writeFrpcConfig(clawId, frpConfig); writeFrpcConfig(clawId, frpConfig, sshSecretKey);
this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], { this._watchdog = new Watchdog('frpc', FRPC_BIN, ['-c', FRPC_CONFIG], {
maxRestarts: 10, maxRestarts: 10,