From 64f40500143aff9772453f8b12e048523088223f Mon Sep 17 00:00:00 2001 From: stswangzhiping <59632378+stswangzhiping@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:54:46 +0800 Subject: [PATCH] fix: EROFS dns config, double WS connection, and watchdog timeout 1. EROFS: install.sh pre-writes DNS hijack config to dnsmasq-shared.d since /etc may be read-only at runtime. dns-hijack.js gracefully falls back to checking if config already exists. 2. Double WS: add _connectionStarted guard to prevent _proceedWithConnection from being called twice (via event + hasInternet check). 3. Watchdog: move _startSdNotify() to start() beginning so READY=1 is sent immediately, not delayed until network is ready. Made-with: Cursor --- .commitmsg | 12 +++++++++++- install.sh | 10 ++++++++++ lib/client.js | 16 ++++++++++------ lib/dns-hijack.js | 44 ++++++++++++++++---------------------------- 4 files changed, 47 insertions(+), 35 deletions(-) diff --git a/.commitmsg b/.commitmsg index 6252121..9043512 100644 --- a/.commitmsg +++ b/.commitmsg @@ -1 +1,11 @@ -docs: add WiFi provisioning user manual to README +fix: EROFS dns config, double WS connection, and watchdog timeout + +1. EROFS: install.sh pre-writes DNS hijack config to dnsmasq-shared.d + since /etc may be read-only at runtime. dns-hijack.js gracefully + falls back to checking if config already exists. + +2. Double WS: add _connectionStarted guard to prevent _proceedWithConnection + from being called twice (via event + hasInternet check). + +3. Watchdog: move _startSdNotify() to start() beginning so READY=1 + is sent immediately, not delayed until network is ready. diff --git a/install.sh b/install.sh index cf6e68c..5d0daa3 100644 --- a/install.sh +++ b/install.sh @@ -55,6 +55,16 @@ if command -v nmcli &>/dev/null; then systemctl enable --now NetworkManager 2>/dev/null || true fi info "NetworkManager ✓" + + # 预写 DNS 劫持配置(运行时 /etc 可能为只读) + NM_DNSMASQ_DIR="/etc/NetworkManager/dnsmasq-shared.d" + mkdir -p "$NM_DNSMASQ_DIR" + cat > "$NM_DNSMASQ_DIR/clawd-captive.conf" << 'DNSCONF' +# clawd captive portal DNS hijack +# All DNS queries resolve to gateway to trigger captive portal +address=/#/10.42.0.1 +DNSCONF + info "DNS 劫持配置已写入 $NM_DNSMASQ_DIR ✓" fi # ── WiFi rfkill 解锁(部分设备默认禁用 WiFi)──────────────────────────────── diff --git a/lib/client.js b/lib/client.js index 40d3b92..3a572c8 100644 --- a/lib/client.js +++ b/lib/client.js @@ -60,12 +60,16 @@ class ClawClient { async start() { log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`); + this._startSdNotify(); + // 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP) this._provisionMgr = new ProvisionManager(this._cfg.claw_id); + this._connectionStarted = false; - // 网络就绪时连接云端 - this._provisionMgr.once('network-ready', () => { - if (!this._ws) { + // 网络就绪时连接云端(仅触发一次) + this._provisionMgr.on('network-ready', () => { + if (!this._connectionStarted) { + this._connectionStarted = true; this._proceedWithConnection().catch(e => { log.error('clawd', '连接启动失败:', e.message); }); @@ -74,8 +78,9 @@ class ClawClient { await this._provisionMgr.start(); - // start() 返回后,如果已有网络,直接连 - if (hasInternet() && !this._ws) { + // start() 返回后,如果已有网络且尚未启动连接 + if (hasInternet() && !this._connectionStarted) { + this._connectionStarted = true; await this._proceedWithConnection(); } else if (!hasInternet()) { log.info('clawd', '等待网络就绪(WiFi 配网或网线接入)...'); @@ -88,7 +93,6 @@ class ClawClient { startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)), ]); this._dashInfo = dashInfo || {}; - this._startSdNotify(); this._connect(); } diff --git a/lib/dns-hijack.js b/lib/dns-hijack.js index 65e4b11..7c4376e 100644 --- a/lib/dns-hijack.js +++ b/lib/dns-hijack.js @@ -6,55 +6,43 @@ const log = require('./logger'); const CAPTIVE_DOMAIN = 'ap.cutos.ai'; -// NetworkManager 的 dnsmasq 共享配置目录 -// NM 启动热点时会自动加载此目录下的 .conf 文件 const NM_DNSMASQ_DIR = '/etc/NetworkManager/dnsmasq-shared.d'; const CAPTIVE_CONF = path.join(NM_DNSMASQ_DIR, 'clawd-captive.conf'); /** - * 通过 NetworkManager 的 dnsmasq 配置实现 DNS 劫持。 + * DNS 劫持管理。 * - * NM 在创建热点时自动启动 dnsmasq 并加载 dnsmasq-shared.d/ 下的配置。 - * 我们只需在启动热点前写入 address=/#/gatewayIp,NM 的 dnsmasq 就会 - * 把所有域名解析到网关 IP,触发 Captive Portal 检测。 - * - * 不再自行管理 dnsmasq 进程,避免与 NM 冲突导致热点被拆除。 + * 利用 NM 的 dnsmasq-shared.d 配置目录实现全域 DNS 劫持。 + * 配置文件由 install.sh 预写(避免运行时 EROFS), + * 运行时仅做验证和兜底写入。 */ class DnsHijack { constructor() { this._active = false; } - /** - * 写入 DNS 劫持配置(需在 startAP 之前调用) - * @param {string} _iface - 接口名(保留参数,兼容调用) - * @param {string} gatewayIp - 网关 IP(如 10.42.0.1) - */ start(_iface, gatewayIp) { - this.stop(); - - const conf = [ - `# clawd captive portal DNS hijack`, - `# All DNS queries resolve to gateway to trigger captive portal`, - `address=/#/${gatewayIp}`, - ].join('\n'); + const conf = `# clawd captive portal DNS hijack\naddress=/#/${gatewayIp}\n`; + // 尝试写入(可能成功也可能 EROFS) try { fs.mkdirSync(NM_DNSMASQ_DIR, { recursive: true }); fs.writeFileSync(CAPTIVE_CONF, conf, 'utf8'); - this._active = true; - log.info('dns', `DNS 劫持配置已写入: ${CAPTIVE_CONF} (${CAPTIVE_DOMAIN} → ${gatewayIp})`); + log.info('dns', `DNS 劫持配置已写入: ${CAPTIVE_CONF}`); } catch (e) { - log.error('dns', `写入 DNS 劫持配置失败: ${e.message}`); + if (fs.existsSync(CAPTIVE_CONF)) { + log.info('dns', 'DNS 劫持配置已存在(install.sh 预写),跳过写入'); + } else { + log.error('dns', `DNS 劫持配置写入失败且不存在: ${e.message}`); + log.error('dns', '请重新运行 install.sh 写入配置'); + } } + this._active = true; } stop() { - if (this._active) { - try { fs.unlinkSync(CAPTIVE_CONF); } catch (_) {} - this._active = false; - log.info('dns', 'DNS 劫持配置已移除'); - } + // 不删除配置文件(保持预写状态,下次启动无需重写) + this._active = false; } }