Convert four more hal modules: bookshelf, unifi, fail2ban, marrytts
- bookshelf: Servarr v1 fork on the radarr template (4 tools). - unifi: portainer-shaped tooled app (7 tools, 9 ports), settings-merged config. - fail2ban: host-level security module mirroring firewall (service + restart-on, no container); ban actions preserved as source ufw/iptables and FLAGGED to be rewritten nftables-native before it actually bans. - marrytts: manifest-only plain container (no tools), like resolv-conf. All typecheck against the built @novox/mesh-sdk; service images digest-pinned. Held from merge pending the hq initialization reconciliation. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
// fail2ban's own code, in the module (novox/hq ADR 0044). The jails and the daemon are declared
|
||||
// resources — the mesh writes /etc/fail2ban/jail.d/* and keeps fail2ban.service running (see
|
||||
// module.json). This code exists only to read and steer the *live* state the daemon owns at
|
||||
// runtime: which IPs are banned right now, and the manual ban/unban an operator reaches for. That
|
||||
// state (the running bans, /var/lib/fail2ban's sqlite) is fail2ban's, not the mesh's — the mesh
|
||||
// reconciles the config, never the ban list.
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
|
||||
export class Fail2banClient {
|
||||
static fromEnv(_env: NodeJS.ProcessEnv = process.env): Fail2banClient {
|
||||
return new Fail2banClient();
|
||||
}
|
||||
|
||||
/** Overview of every jail, or the detailed status of one — currently-banned IPs and totals. */
|
||||
async status(jail?: string): Promise<string> {
|
||||
if (jail) {
|
||||
const { stdout } = await run("sudo", ["fail2ban-client", "status", jail]);
|
||||
return stdout;
|
||||
}
|
||||
const { stdout: overview } = await run("sudo", ["fail2ban-client", "status"]);
|
||||
const match = overview.match(/Jail list:\s*(.+)/);
|
||||
if (!match) return overview;
|
||||
|
||||
const jails = match[1].split(",").map((j) => j.trim()).filter(Boolean);
|
||||
const parts: string[] = [overview.trimEnd(), ""];
|
||||
for (const j of jails) {
|
||||
const { stdout } = await run("sudo", ["fail2ban-client", "status", j]);
|
||||
parts.push(`=== ${j} ===`, stdout.trimEnd(), "");
|
||||
}
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
/** Manually ban an IP in a jail. Mutates live state, not a mesh-managed file. */
|
||||
async ban(jail: string, ip: string): Promise<string> {
|
||||
const { stdout } = await run("sudo", ["fail2ban-client", "set", jail, "banip", ip]);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
/** Unban an IP from one jail, or from every jail when no jail is given. */
|
||||
async unban(ip: string, jail?: string): Promise<string> {
|
||||
const args = jail
|
||||
? ["fail2ban-client", "set", jail, "unbanip", ip]
|
||||
: ["fail2ban-client", "unban", ip];
|
||||
const { stdout } = await run("sudo", args);
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user