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
This commit is contained in:
12
.commitmsg
12
.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.
|
||||||
|
|||||||
10
install.sh
10
install.sh
@@ -55,6 +55,16 @@ if command -v nmcli &>/dev/null; then
|
|||||||
systemctl enable --now NetworkManager 2>/dev/null || true
|
systemctl enable --now NetworkManager 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
info "NetworkManager ✓"
|
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
|
fi
|
||||||
|
|
||||||
# ── WiFi rfkill 解锁(部分设备默认禁用 WiFi)────────────────────────────────
|
# ── WiFi rfkill 解锁(部分设备默认禁用 WiFi)────────────────────────────────
|
||||||
|
|||||||
@@ -60,12 +60,16 @@ class ClawClient {
|
|||||||
async start() {
|
async start() {
|
||||||
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
log.info('clawd', `启动中... 服务器 = ${this._cfg.server}`);
|
||||||
|
|
||||||
|
this._startSdNotify();
|
||||||
|
|
||||||
// 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP)
|
// 启动 AP 配网管理器(等待已保存 WiFi 自动连接,超时再开 AP)
|
||||||
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
|
this._provisionMgr = new ProvisionManager(this._cfg.claw_id);
|
||||||
|
this._connectionStarted = false;
|
||||||
|
|
||||||
// 网络就绪时连接云端
|
// 网络就绪时连接云端(仅触发一次)
|
||||||
this._provisionMgr.once('network-ready', () => {
|
this._provisionMgr.on('network-ready', () => {
|
||||||
if (!this._ws) {
|
if (!this._connectionStarted) {
|
||||||
|
this._connectionStarted = true;
|
||||||
this._proceedWithConnection().catch(e => {
|
this._proceedWithConnection().catch(e => {
|
||||||
log.error('clawd', '连接启动失败:', e.message);
|
log.error('clawd', '连接启动失败:', e.message);
|
||||||
});
|
});
|
||||||
@@ -74,8 +78,9 @@ class ClawClient {
|
|||||||
|
|
||||||
await this._provisionMgr.start();
|
await this._provisionMgr.start();
|
||||||
|
|
||||||
// start() 返回后,如果已有网络,直接连
|
// start() 返回后,如果已有网络且尚未启动连接
|
||||||
if (hasInternet() && !this._ws) {
|
if (hasInternet() && !this._connectionStarted) {
|
||||||
|
this._connectionStarted = true;
|
||||||
await this._proceedWithConnection();
|
await this._proceedWithConnection();
|
||||||
} else if (!hasInternet()) {
|
} else if (!hasInternet()) {
|
||||||
log.info('clawd', '等待网络就绪(WiFi 配网或网线接入)...');
|
log.info('clawd', '等待网络就绪(WiFi 配网或网线接入)...');
|
||||||
@@ -88,7 +93,6 @@ class ClawClient {
|
|||||||
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
|
startTtyd().catch(e => log.warn('ttyd', '启动失败:', e.message)),
|
||||||
]);
|
]);
|
||||||
this._dashInfo = dashInfo || {};
|
this._dashInfo = dashInfo || {};
|
||||||
this._startSdNotify();
|
|
||||||
this._connect();
|
this._connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,55 +6,43 @@ const log = require('./logger');
|
|||||||
|
|
||||||
const CAPTIVE_DOMAIN = 'ap.cutos.ai';
|
const CAPTIVE_DOMAIN = 'ap.cutos.ai';
|
||||||
|
|
||||||
// NetworkManager 的 dnsmasq 共享配置目录
|
|
||||||
// NM 启动热点时会自动加载此目录下的 .conf 文件
|
|
||||||
const NM_DNSMASQ_DIR = '/etc/NetworkManager/dnsmasq-shared.d';
|
const NM_DNSMASQ_DIR = '/etc/NetworkManager/dnsmasq-shared.d';
|
||||||
const CAPTIVE_CONF = path.join(NM_DNSMASQ_DIR, 'clawd-captive.conf');
|
const CAPTIVE_CONF = path.join(NM_DNSMASQ_DIR, 'clawd-captive.conf');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过 NetworkManager 的 dnsmasq 配置实现 DNS 劫持。
|
* DNS 劫持管理。
|
||||||
*
|
*
|
||||||
* NM 在创建热点时自动启动 dnsmasq 并加载 dnsmasq-shared.d/ 下的配置。
|
* 利用 NM 的 dnsmasq-shared.d 配置目录实现全域 DNS 劫持。
|
||||||
* 我们只需在启动热点前写入 address=/#/gatewayIp,NM 的 dnsmasq 就会
|
* 配置文件由 install.sh 预写(避免运行时 EROFS),
|
||||||
* 把所有域名解析到网关 IP,触发 Captive Portal 检测。
|
* 运行时仅做验证和兜底写入。
|
||||||
*
|
|
||||||
* 不再自行管理 dnsmasq 进程,避免与 NM 冲突导致热点被拆除。
|
|
||||||
*/
|
*/
|
||||||
class DnsHijack {
|
class DnsHijack {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._active = false;
|
this._active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 写入 DNS 劫持配置(需在 startAP 之前调用)
|
|
||||||
* @param {string} _iface - 接口名(保留参数,兼容调用)
|
|
||||||
* @param {string} gatewayIp - 网关 IP(如 10.42.0.1)
|
|
||||||
*/
|
|
||||||
start(_iface, gatewayIp) {
|
start(_iface, gatewayIp) {
|
||||||
this.stop();
|
const conf = `# clawd captive portal DNS hijack\naddress=/#/${gatewayIp}\n`;
|
||||||
|
|
||||||
const conf = [
|
|
||||||
`# clawd captive portal DNS hijack`,
|
|
||||||
`# All DNS queries resolve to gateway to trigger captive portal`,
|
|
||||||
`address=/#/${gatewayIp}`,
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
|
// 尝试写入(可能成功也可能 EROFS)
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(NM_DNSMASQ_DIR, { recursive: true });
|
fs.mkdirSync(NM_DNSMASQ_DIR, { recursive: true });
|
||||||
fs.writeFileSync(CAPTIVE_CONF, conf, 'utf8');
|
fs.writeFileSync(CAPTIVE_CONF, conf, 'utf8');
|
||||||
this._active = true;
|
log.info('dns', `DNS 劫持配置已写入: ${CAPTIVE_CONF}`);
|
||||||
log.info('dns', `DNS 劫持配置已写入: ${CAPTIVE_CONF} (${CAPTIVE_DOMAIN} → ${gatewayIp})`);
|
|
||||||
} catch (e) {
|
} 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() {
|
stop() {
|
||||||
if (this._active) {
|
// 不删除配置文件(保持预写状态,下次启动无需重写)
|
||||||
try { fs.unlinkSync(CAPTIVE_CONF); } catch (_) {}
|
|
||||||
this._active = false;
|
this._active = false;
|
||||||
log.info('dns', 'DNS 劫持配置已移除');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user