fix: stop killing NM's dnsmasq - use dnsmasq-shared.d instead
Killing all dnsmasq processes caused NetworkManager to detect its hotspot dnsmasq died and tear down the hotspot (AP appears briefly then disappears). Now leverage NM's built-in dnsmasq-shared.d config directory: write DNS hijack config before starting AP so NM's own dnsmasq picks it up. No separate dnsmasq process needed. Made-with: Cursor
This commit is contained in:
14
.commitmsg
14
.commitmsg
@@ -1,9 +1,9 @@
|
|||||||
fix: wait for NM auto-reconnect before starting AP on reboot
|
fix: stop killing NM's dnsmasq - use dnsmasq-shared.d instead
|
||||||
|
|
||||||
After WiFi is configured and device reboots, NetworkManager needs
|
Killing all dnsmasq processes caused NetworkManager to detect its
|
||||||
a few seconds to auto-connect to saved WiFi. Without waiting,
|
hotspot dnsmasq died and tear down the hotspot (AP appears briefly
|
||||||
AP starts immediately and occupies wlan0, preventing NM reconnect.
|
then disappears).
|
||||||
|
|
||||||
- Add hasSavedWifiConnection() to check for saved WiFi profiles
|
Now leverage NM's built-in dnsmasq-shared.d config directory:
|
||||||
- Wait up to 20s for NM auto-connect before falling back to AP
|
write DNS hijack config before starting AP so NM's own dnsmasq
|
||||||
- First boot (no saved WiFi) still starts AP immediately
|
picks it up. No separate dnsmasq process needed.
|
||||||
|
|||||||
@@ -1,112 +1,61 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { spawn, execSync } = require('child_process');
|
const fs = require('fs');
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const log = require('./logger');
|
const log = require('./logger');
|
||||||
const { Watchdog } = require('./watchdog');
|
|
||||||
|
|
||||||
const CONFIG_DIR = process.env.CLAWD_CONFIG_DIR
|
|
||||||
|| (process.getuid && process.getuid() === 0 ? '/etc/clawd' : path.join(os.homedir(), '.clawd'));
|
|
||||||
|
|
||||||
const DNSMASQ_CONF = path.join(CONFIG_DIR, 'dnsmasq-captive.conf');
|
|
||||||
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 CAPTIVE_CONF = path.join(NM_DNSMASQ_DIR, 'clawd-captive.conf');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理 dnsmasq 进程:
|
* 通过 NetworkManager 的 dnsmasq 配置实现 DNS 劫持。
|
||||||
* - 所有 DNS 查询 → 网关 IP(触发 Captive Portal 弹窗)
|
*
|
||||||
* - DHCP 分配 10.42.0.50 ~ 10.42.0.150
|
* NM 在创建热点时自动启动 dnsmasq 并加载 dnsmasq-shared.d/ 下的配置。
|
||||||
* - ap.cutos.ai 专门指向网关
|
* 我们只需在启动热点前写入 address=/#/gatewayIp,NM 的 dnsmasq 就会
|
||||||
|
* 把所有域名解析到网关 IP,触发 Captive Portal 检测。
|
||||||
|
*
|
||||||
|
* 不再自行管理 dnsmasq 进程,避免与 NM 冲突导致热点被拆除。
|
||||||
*/
|
*/
|
||||||
class DnsHijack {
|
class DnsHijack {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._watchdog = null;
|
this._active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动 dnsmasq
|
* 写入 DNS 劫持配置(需在 startAP 之前调用)
|
||||||
* @param {string} iface - AP 接口名(如 wlan0)
|
* @param {string} _iface - 接口名(保留参数,兼容调用)
|
||||||
* @param {string} gatewayIp - 网关 IP(如 10.42.0.1)
|
* @param {string} gatewayIp - 网关 IP(如 10.42.0.1)
|
||||||
*/
|
*/
|
||||||
start(iface, gatewayIp) {
|
start(_iface, gatewayIp) {
|
||||||
this.stop();
|
this.stop();
|
||||||
|
|
||||||
const rangeStart = gatewayIp.replace(/\.\d+$/, '.50');
|
|
||||||
const rangeEnd = gatewayIp.replace(/\.\d+$/, '.150');
|
|
||||||
|
|
||||||
const conf = [
|
const conf = [
|
||||||
`interface=${iface}`,
|
`# clawd captive portal DNS hijack`,
|
||||||
'bind-interfaces',
|
`# All DNS queries resolve to gateway to trigger captive portal`,
|
||||||
`listen-address=${gatewayIp}`,
|
|
||||||
'',
|
|
||||||
'# DHCP',
|
|
||||||
`dhcp-range=${rangeStart},${rangeEnd},255.255.255.0,12h`,
|
|
||||||
`dhcp-option=3,${gatewayIp}`, // gateway
|
|
||||||
`dhcp-option=6,${gatewayIp}`, // DNS server
|
|
||||||
'',
|
|
||||||
'# DNS: 所有域名指向网关(触发 Captive Portal 检测)',
|
|
||||||
`address=/#/${gatewayIp}`,
|
`address=/#/${gatewayIp}`,
|
||||||
'',
|
|
||||||
'# 日志',
|
|
||||||
'log-queries',
|
|
||||||
'log-facility=-', // stdout → Watchdog 采集
|
|
||||||
'',
|
|
||||||
'# 禁用系统 resolv.conf',
|
|
||||||
'no-resolv',
|
|
||||||
'no-poll',
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
try {
|
||||||
fs.writeFileSync(DNSMASQ_CONF, conf, 'utf8');
|
fs.mkdirSync(NM_DNSMASQ_DIR, { recursive: true });
|
||||||
log.info('dns', `dnsmasq 配置已写入: ${DNSMASQ_CONF}`);
|
fs.writeFileSync(CAPTIVE_CONF, conf, 'utf8');
|
||||||
|
this._active = true;
|
||||||
// 终止所有 dnsmasq(包括 NetworkManager 自动启动的)
|
log.info('dns', `DNS 劫持配置已写入: ${CAPTIVE_CONF} (${CAPTIVE_DOMAIN} → ${gatewayIp})`);
|
||||||
try { execSync('pkill -9 dnsmasq', { timeout: 3000 }); } catch (_) {}
|
} catch (e) {
|
||||||
// 等待端口释放
|
log.error('dns', `写入 DNS 劫持配置失败: ${e.message}`);
|
||||||
try { execSync('sleep 1'); } catch (_) {}
|
|
||||||
|
|
||||||
// 查找 dnsmasq 二进制(/usr/sbin 可能不在普通用户 PATH 中)
|
|
||||||
const dnsmasqBin = findBin('dnsmasq');
|
|
||||||
if (!dnsmasqBin) {
|
|
||||||
log.error('dns', 'dnsmasq 未安装,请运行: apt install dnsmasq');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._watchdog = new Watchdog('dns', dnsmasqBin, [
|
|
||||||
'--no-daemon',
|
|
||||||
`--conf-file=${DNSMASQ_CONF}`,
|
|
||||||
], {
|
|
||||||
maxRestarts: 5,
|
|
||||||
windowMs: 60_000,
|
|
||||||
restartDelay: 2_000,
|
|
||||||
onStdout: (line) => log.debug('dns', line),
|
|
||||||
onStderr: (line) => log.debug('dns', line),
|
|
||||||
});
|
|
||||||
this._watchdog.start();
|
|
||||||
log.info('dns', `dnsmasq 已启动: ${CAPTIVE_DOMAIN} → ${gatewayIp}, 全域劫持`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
if (this._watchdog) {
|
if (this._active) {
|
||||||
this._watchdog.stop();
|
try { fs.unlinkSync(CAPTIVE_CONF); } catch (_) {}
|
||||||
this._watchdog = null;
|
this._active = false;
|
||||||
|
log.info('dns', 'DNS 劫持配置已移除');
|
||||||
}
|
}
|
||||||
try { execSync('pkill -9 dnsmasq', { timeout: 3000 }); } catch (_) {}
|
|
||||||
try { fs.unlinkSync(DNSMASQ_CONF); } catch (_) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findBin(name) {
|
|
||||||
const searchPaths = ['/usr/sbin', '/usr/bin', '/sbin', '/bin', '/usr/local/sbin', '/usr/local/bin'];
|
|
||||||
for (const dir of searchPaths) {
|
|
||||||
const full = path.join(dir, name);
|
|
||||||
if (fs.existsSync(full)) return full;
|
|
||||||
}
|
|
||||||
// 兜底尝试 which(PATH 可能已包含)
|
|
||||||
try {
|
|
||||||
return execSync(`which ${name}`, { encoding: 'utf8', timeout: 3000 }).trim() || null;
|
|
||||||
} catch (_) { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { DnsHijack, CAPTIVE_DOMAIN };
|
module.exports = { DnsHijack, CAPTIVE_DOMAIN };
|
||||||
|
|||||||
@@ -98,10 +98,12 @@ class ProvisionManager extends EventEmitter {
|
|||||||
if (this._state === 'ap') return;
|
if (this._state === 'ap') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ap = startAP(this._clawId);
|
// 先写 DNS 劫持配置,再启动 AP
|
||||||
|
// NM 启动热点时会加载 dnsmasq-shared.d/ 下的配置
|
||||||
this._dns = new DnsHijack();
|
this._dns = new DnsHijack();
|
||||||
this._dns.start(ap.iface, AP_IP);
|
this._dns.start('wlan0', AP_IP);
|
||||||
|
|
||||||
|
const ap = startAP(this._clawId);
|
||||||
|
|
||||||
this._server = new CaptiveServer({
|
this._server = new CaptiveServer({
|
||||||
clawId: this._clawId,
|
clawId: this._clawId,
|
||||||
|
|||||||
Reference in New Issue
Block a user