Files
op-packages/minigate/tests/lan-dns.test.cjs
T

173 lines
8.3 KiB
JavaScript

const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const root = path.resolve(__dirname, '..');
const script = path.join(root, 'root/usr/lib/minigate/proxy.sh');
const HEADER = '# MiniGate 局域网直连解析,由 proxy.sh 自动生成';
const SITES = {
'000_default_2000_1_1_.conf': 'server {\n listen unix:/var/run/minigate/https_2000.sock default_server ssl proxy_protocol;\n server_name _;\n return 444;\n}\n',
'site_1.conf': 'server {\n listen unix:/var/run/minigate/https_2000.sock ssl proxy_protocol;\n server_name ly.example.com;\n}\nserver {\n listen unix:/var/run/minigate/http_2000.sock proxy_protocol;\n server_name ly.example.com;\n return 308 https://ly.example.com:2000$request_uri;\n}\n',
'site_2.conf': 'server {\n listen 2000 ssl;\n server_name box.example.com;\n}\n',
'site_3.conf': 'server {\n listen 443 ssl;\n server_name *.wild.example.com;\n}\n'
};
function sandbox({ sites = SITES, hosts } = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lan-dns-test-'));
const sitesDir = path.join(dir, 'sites');
fs.mkdirSync(sitesDir);
for (const [name, content] of Object.entries(sites)) fs.writeFileSync(path.join(sitesDir, name), content);
const hostsFile = path.join(dir, 'hosts', 'minigate');
if (hosts !== undefined) {
fs.mkdirSync(path.dirname(hostsFile));
fs.writeFileSync(hostsFile, hosts);
}
return { dir, sitesDir, hostsFile, hupLog: path.join(dir, 'hup.log'), logFile: path.join(dir, 'proxy.log') };
}
// 以库模式加载 proxy.sh,替换 uci/ubus/pidof/send_hup 后执行一段命令
function runLibrary(box, body, { lanDns = '1', lanIp = '', ifaceIp = '192.168.2.2', networkIp = '' } = {}) {
const command = `
set -u
export MINIGATE_PROXY_LIBRARY_ONLY=1
export MINIGATE_SITES_DIR=${JSON.stringify(box.sitesDir)}
export MINIGATE_LAN_HOSTS_FILE=${JSON.stringify(box.hostsFile)}
export MINIGATE_LOGFILE=${JSON.stringify(box.logFile)}
. ${JSON.stringify(script)}
uci() {
[ "\${1:-}" = "-q" ] && shift
case "\${1:-}:\${2:-}" in
get:minigate.global.lan_dns) printf '%s\\n' ${JSON.stringify(lanDns)} ;;
get:minigate.global.lan_ip) [ -n ${JSON.stringify(lanIp)} ] && printf '%s\\n' ${JSON.stringify(lanIp)} ;;
get:network.lan.ipaddr) [ -n ${JSON.stringify(networkIp)} ] && printf '%s\\n' ${JSON.stringify(networkIp)} ;;
esac
return 0
}
ubus() { [ -n ${JSON.stringify(ifaceIp)} ] && printf '%s\\n' ${JSON.stringify(ifaceIp)}; }
jsonfilter() { cat; }
pidof() { printf '101 102\\n'; }
send_hup() { printf '%s\\n' "$*" >> ${JSON.stringify(box.hupLog)}; }
${body}
`;
return spawnSync('/bin/sh', ['-c', command], { encoding: 'utf8' });
}
const read = file => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
const hups = box => (read(box.hupLog) || '').split('\n').filter(Boolean);
test('enabled sync maps every concrete site domain to the LAN address and reloads dnsmasq once', () => {
const box = sandbox();
let result = runLibrary(box, 'sync_lan_hosts');
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), `${HEADER}\n192.168.2.2 box.example.com\n192.168.2.2 ly.example.com\n`);
assert.deepEqual(hups(box), ['101 102']);
assert.match(read(box.logFile), /局域网直连解析 -> 192\.168\.2\.2: box\.example\.com ly\.example\.com/);
// 内容不变时不再重载,避免每次保存都清空 dnsmasq 缓存
result = runLibrary(box, 'sync_lan_hosts');
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(hups(box), ['101 102']);
});
test('a changed address rewrites the file and reloads again', () => {
const box = sandbox({ hosts: `${HEADER}\n192.168.2.9 box.example.com\n192.168.2.9 ly.example.com\n` });
const result = runLibrary(box, 'sync_lan_hosts');
assert.equal(result.status, 0, result.stderr);
assert.match(read(box.hostsFile), /^192\.168\.2\.2 box\.example\.com$/m);
assert.deepEqual(hups(box), ['101 102']);
});
test('a manual LAN address takes priority over the interface address', () => {
const box = sandbox();
const result = runLibrary(box, 'sync_lan_hosts', { lanIp: '10.0.0.5' });
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), `${HEADER}\n10.0.0.5 box.example.com\n10.0.0.5 ly.example.com\n`);
});
test('without interface status the first network.lan.ipaddr entry is used without its prefix', () => {
const box = sandbox();
const result = runLibrary(box, 'get_lan_ip', { ifaceIp: '', networkIp: '192.168.9.1/24 192.168.10.1/24' });
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout.trim(), '192.168.9.1');
});
test('an invalid LAN address writes nothing and removes a stale file', () => {
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
const result = runLibrary(box, 'sync_lan_hosts', { lanIp: '192.168.1.300' });
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), null);
assert.deepEqual(hups(box), ['101 102']);
assert.match(read(box.logFile), /未取得有效的 LAN IPv4 地址/);
});
test('disabling removes the file once and does not reload when nothing was written', () => {
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
let result = runLibrary(box, 'sync_lan_hosts', { lanDns: '0' });
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), null);
assert.deepEqual(hups(box), ['101 102']);
result = runLibrary(box, 'sync_lan_hosts', { lanDns: '0' });
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(hups(box), ['101 102']);
});
test('no generated sites means no hosts file', () => {
const box = sandbox({ sites: { '000_default_443_1_0_.conf': 'server {\n server_name _;\n}\n' } });
const result = runLibrary(box, 'sync_lan_hosts');
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), null);
assert.deepEqual(hups(box), []);
});
test('reload syncs LAN hosts only after nginx accepted the new configuration', () => {
const box = sandbox();
const body = ok => `
PID_FILE=${JSON.stringify(path.join(box.dir, 'missing.pid'))}
prepare_config() { return 0; }
start_prepared() { return ${ok ? 0 : 1}; }
sync_lan_hosts() { echo synced; }
do_reload
`;
let result = runLibrary(box, body(true));
assert.equal(result.status, 0, result.stderr);
assert.equal(result.stdout.trim(), 'synced');
result = runLibrary(box, body(false));
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
});
test('proxy.sh stop removes the LAN hosts file', () => {
const box = sandbox({ hosts: `${HEADER}\n192.168.2.2 box.example.com\n` });
const bin = path.join(box.dir, 'bin');
fs.mkdirSync(bin);
fs.writeFileSync(path.join(bin, 'pidof'), '#!/bin/sh\nexit 1\n', { mode: 0o755 });
const result = spawnSync('/bin/sh', [script, 'stop'], {
encoding: 'utf8',
env: {
...process.env,
PATH: `${bin}:${process.env.PATH}`,
MINIGATE_PID_FILE: path.join(box.dir, 'missing.pid'),
MINIGATE_LAN_HOSTS_FILE: box.hostsFile,
MINIGATE_LOGFILE: box.logFile
}
});
assert.equal(result.status, 0, result.stderr);
assert.equal(read(box.hostsFile), null);
assert.match(read(box.logFile), /局域网直连解析已移除/);
});
test('LuCI exposes the toggle and an optional IPv4 address, disabled by default', () => {
const page = fs.readFileSync(path.join(root, 'luasrc/model/cbi/minigate/proxy.lua'), 'utf8');
const config = fs.readFileSync(path.join(root, 'root/etc/config/minigate'), 'utf8');
assert.match(page, /s:option\(Flag, "lan_dns", "启用局域网直连解析"\)\no\.default = "0"/);
assert.match(page, /s:option\(Value, "lan_ip",[^\n]*\)\no\.datatype = "ip4addr"/);
assert.match(page, /o:depends\("lan_dns", "1"\)/);
assert.match(config, /option lan_dns '0'/);
});