🏅 Sync 2026-10-29 12:54:11

This commit is contained in:
github-actions[bot] committed 2026-10-29 12:54:11 +08:00
1 parent 96c15c82a4
commit 5536ec1272
34 files changed
+579 -158

No files matched your search

+60
View File
@@ -213,3 +213,63 @@ test('reaching the threshold bans the attacking address even when other bans are
assert.equal(fs.existsSync(path.join(dir, 'run/counters/139.162.161.162')), false, 'counter is cleared after a ban');
}
});
function guardLibrary(dir, body) {
return spawnSync('/bin/sh', ['-c', `
logger() { :; }
uci() { case "$3" in minigate.login_guard.threshold) echo 3 ;; *) return 1 ;; esac; }
nft() {
printf '%s\\n' "$*" >> ${JSON.stringify(path.join(dir, 'nft.log'))}
case "$*" in
'list set inet fw4 login_banned_v4'|'list set inet fw4 login_banned_v6') [ -f ${JSON.stringify(path.join(dir, 'sets-empty'))} ] || echo 'elements = { 1.1.1.1 }' ;;
'list chain inet fw4 input') echo '@login_banned_v4 @login_banned_v6' ;;
esac
return 0
}
export MINIGATE_LG_LIBRARY_ONLY=1 MINIGATE_LG_DATA_DIR=${JSON.stringify(path.join(dir, 'data'))}
export MINIGATE_LG_RUN_DIR=${JSON.stringify(path.join(dir, 'run'))} MINIGATE_LG_LOG=${JSON.stringify(path.join(dir, 'lg.log'))}
. ${JSON.stringify(script)}
${body}
`], { encoding: 'utf8' });
}
test('bans and failure counters record whether the attempt came over SSH or LuCI', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-kind-'));
fs.mkdirSync(path.join(dir, 'data'));
fs.writeFileSync(path.join(dir, 'data/bans.txt'), '111.170.34.182 9999999999\n');
const result = guardLibrary(dir, `
handle_failure 198.51.100.30 luci
cat "$COUNTER_DIR/198.51.100.30"
for i in 1 2 3; do handle_failure 139.162.161.162 ssh; done
cmd_ban 198.51.100.40 >/dev/null
`);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /^\d+ 1 luci$/m);
const bans = fs.readFileSync(path.join(dir, 'data/bans.txt'), 'utf8');
assert.match(bans, /^111\.170\.34\.182 9999999999$/m, 'old records without a type are kept as they are');
assert.match(bans, /^139\.162\.161\.162 \d+ ssh$/m);
assert.match(bans, /^198\.51\.100\.40 \d+ manual$/m);
const log = fs.readFileSync(path.join(dir, 'lg.log'), 'utf8');
assert.match(log, /FAIL 198\.51\.100\.30 LuCI \(1\/3\)/);
assert.match(log, /BAN 139\.162\.161\.162 12小时(SSH,达到 3 次失败)/);
});
test('typed ban records survive a firewall reload and are restored with their type', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-kind-restore-'));
fs.mkdirSync(path.join(dir, 'data'));
fs.writeFileSync(path.join(dir, 'sets-empty'), '');
fs.writeFileSync(path.join(dir, 'data/bans.txt'), '139.162.161.162 9999999999 ssh\n2001:db8::9 9999999999 luci\n5.231.242.181 9999999999\n');
const result = guardLibrary(dir, 'repair_nft');
assert.equal(result.status, 0, result.stderr);
const calls = fs.readFileSync(path.join(dir, 'nft.log'), 'utf8');
assert.match(calls, /add element inet fw4 login_banned_v4 \{ 139\.162\.161\.162 timeout \d+s \}/);
assert.match(calls, /add element inet fw4 login_banned_v6 \{ 2001:db8::9 timeout \d+s \}/);
assert.equal(fs.readFileSync(path.join(dir, 'data/bans.txt'), 'utf8'), '139.162.161.162 9999999999 ssh\n2001:db8::9 9999999999 luci\n5.231.242.181 9999999999\n');
});
test('each log source reports its own access type', () => {
const source = fs.readFileSync(script, 'utf8');
assert.match(source, /handle_failure "\$\(extract_ip "\$line" 1\)" ssh/);
assert.match(source, /handle_failure "\$\(extract_ip "\$line" 0\)" luci/);
assert.match(source, /handle_failure "\$ip" luci/);
});
+24
View File
@@ -208,3 +208,27 @@ test('LuCI forms validate domains, Cloudflare IDs, intervals, thresholds and whi
'range(1,100)\ttrue\tfalse\tfalse'
].join('\n'));
});
test('login guard status adds the access type from the persisted ban list and counters', { skip: !lua }, () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'minigate-lg-status-'));
fs.writeFileSync(path.join(dir, 'bans.txt'), '103.160.205.37 1795830696 ssh\n5.231.242.181 1795794101\n');
const prelude = controllerPrelude.replace("get=function(_, _, sec) if sec == 'cfg01' then return 'ddns' end end",
"get=function(_, _, sec) local v; if sec == 'cfg01' then v = 'ddns' end; return v end");
assert.notEqual(prelude, controllerPrelude);
const out = runLua(prelude.replace("exec=function() return '' end", `exec=function(cmd)
if cmd:find('login_banned_v4', 1, true) then
return '{"nftables": [{"set": {"elem": [{"elem": {"val": "103.160.205.37", "timeout": 2592000, "expires": 3600}}, {"elem": {"val": "5.231.242.181", "timeout": 2592000, "expires": 60}}]}}]}'
end
return ''
end`) + `
local real_open = io.open
io.open = function(file, mode)
if file == '/etc/minigate/login-guard/bans.txt' then file = ${JSON.stringify(path.join(dir, 'bans.txt'))} end
return real_open(file, mode)
end
package.loaded['nixio.fs'] = {dir=function() return nil end, stat=function() return nil end}
c.action_lg_status()
for _, b in ipairs(response.banned) do print(b.ip, b.remaining, b.kind == '' and '(none)' or b.kind) end
`);
assert.equal(out, '103.160.205.37\t3600\tssh\n5.231.242.181\t60\t(none)');
});
+23
View File
@@ -248,3 +248,26 @@ test('state-changing LuCI endpoints require POST with the CSRF token', () => {
assert.match(controller, /"acme_job"\}, call\("action_acme_job"\)\)/);
assert.match(controller, /sec:match\("\^\[%w_\]\+\$"\)/);
});
test('login guard lists show whether each address failed over SSH, LuCI or was banned by hand', () => {
const page = fakePage(loginGuardScript);
take(page, 'lu').callback(null, {
enabled: '1', running: true, threshold: 3,
banned: [
{ ip: '103.160.205.37', remaining: 3600, kind: 'ssh' },
{ ip: '198.51.100.9', remaining: 600, kind: 'manual' },
{ ip: '5.231.242.181', remaining: 60, kind: '' }
],
banned_total: 3,
watching: [{ ip: '198.51.100.20', kind: 'luci', count: 1, age: 5, last_seen: '2026-10-29 12:00:00' }],
watching_total: 1
});
const banned = page.element('lg-banned-list').innerHTML;
assert.match(banned, /<th>IP 地址<\/th><th>访问类型<\/th><th>归属地<\/th>/);
assert.match(banned, /103\.160\.205\.37<\/code><\/td><td><span class="lg-kind lg-kind-ssh">SSH<\/span>/);
assert.match(banned, /lg-kind-manual">手动</);
assert.match(banned, /5\.231\.242\.181<\/code><\/td><td><span class="lg-kind lg-kind-none">未记录</);
const watching = page.element('lg-watching-list').innerHTML;
assert.match(watching, /<th>访问类型<\/th>/);
assert.match(watching, /198\.51\.100\.20<\/code><\/td><td><span class="lg-kind lg-kind-luci">LuCI</);
});