mirror of
https://github.com/YuzuZensai/Termix.git
synced 2026-09-13 18:58:52 +00:00
130 lines
4.0 KiB
TypeScript
130 lines
4.0 KiB
TypeScript
import type { Express } from "express";
|
|||
|
|
import { collectFirewallMetrics } from "../widgets/firewall-collector.js";
|
||
|
|
import { execElevated } from "./exec-elevated.js";
|
||
|
|
import { managerHandler, ManagerInputError } from "./route-helpers.js";
|
||
|
|
import {
|
||
|
|
isValidPort,
|
||
|
|
isValidIpProtocol,
|
||
|
|
isValidFirewallTarget,
|
||
|
|
type IpProtocol,
|
||
|
|
type FirewallTarget,
|
||
|
|
} from "./validation.js";
|
||
|
|
import { detectPlatform } from "./platform.js";
|
||
|
|
import type { ManagerRoutesDeps } from "./types.js";
|
||
|
|
|
||
|
|
export interface FirewallRuleSpec {
|
||
|
|
protocol: IpProtocol;
|
||
|
|
port: number;
|
||
|
|
target: FirewallTarget;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Build an iptables add/delete for an INPUT rule. We only ever touch INPUT for a
|
||
|
|
* specific dport with an explicit target, and never the chain policy, so an
|
||
|
|
* existing ESTABLISHED/SSH rule is left intact.
|
||
|
|
*/
|
||
|
|
export function buildIptablesRuleCommand(
|
||
|
|
op: "add" | "delete",
|
||
|
|
spec: FirewallRuleSpec,
|
||
|
|
): string {
|
||
|
|
const flag = op === "add" ? "-A" : "-D";
|
||
|
|
return `iptables ${flag} INPUT -p ${spec.protocol} --dport ${spec.port} -j ${spec.target}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function buildNftRuleCommand(
|
||
|
|
op: "add" | "delete",
|
||
|
|
spec: FirewallRuleSpec,
|
||
|
|
): string {
|
||
|
|
// nftables uses the inet filter table's input chain by convention.
|
||
|
|
const verb = op === "add" ? "add" : "delete";
|
||
|
|
const action =
|
||
|
|
spec.target.toLowerCase() === "reject"
|
||
|
|
? "reject"
|
||
|
|
: spec.target.toLowerCase();
|
||
|
|
return `nft ${verb} rule inet filter input ${spec.protocol} dport ${spec.port} ${action}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function registerFirewallRoutes(
|
||
|
|
app: Express,
|
||
|
|
{ validateHostId, runOnHost }: ManagerRoutesDeps,
|
||
|
|
): void {
|
||
|
|
app.get(
|
||
|
|
"/host-metrics/managers/firewall/:id",
|
||
|
|
validateHostId,
|
||
|
|
managerHandler(runOnHost, "read", "firewall_read", async (client) => {
|
||
|
|
return await collectFirewallMetrics(client);
|
||
|
|
}),
|
||
|
|
);
|
||
|
|
|
||
|
|
app.post(
|
||
|
|
"/host-metrics/managers/firewall/:id/rule",
|
||
|
|
validateHostId,
|
||
|
|
managerHandler(
|
||
|
|
runOnHost,
|
||
|
|
"execute",
|
||
|
|
"firewall_rule",
|
||
|
|
async (client, host, req) => {
|
||
|
|
const { op, protocol, port, target } = req.body as {
|
||
|
|
op?: "add" | "delete";
|
||
|
|
protocol?: string;
|
||
|
|
port?: number;
|
||
|
|
target?: string;
|
||
|
|
};
|
||
|
|
if (op !== "add" && op !== "delete") {
|
||
|
|
throw new ManagerInputError("Invalid op");
|
||
|
|
}
|
||
|
|
if (!isValidIpProtocol(protocol))
|
||
|
|
throw new ManagerInputError("Invalid protocol");
|
||
|
|
if (!isValidPort(port)) throw new ManagerInputError("Invalid port");
|
||
|
|
if (!isValidFirewallTarget(target))
|
||
|
|
throw new ManagerInputError("Invalid target");
|
||
|
|
|
||
|
|
const spec: FirewallRuleSpec = {
|
||
|
|
protocol,
|
||
|
|
port: Number(port),
|
||
|
|
target,
|
||
|
|
};
|
||
|
|
const fw = await collectFirewallMetrics(client);
|
||
|
|
const cmd =
|
||
|
|
fw.type === "nftables"
|
||
|
|
? buildNftRuleCommand(op, spec)
|
||
|
|
: buildIptablesRuleCommand(op, spec);
|
||
|
|
const result = await execElevated(client, cmd, host.sudoPassword, {
|
||
|
|
forceSudo: true,
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
success: result.code === 0,
|
||
|
|
output: result.stdout || result.stderr,
|
||
|
|
backend: fw.type,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
app.post(
|
||
|
|
"/host-metrics/managers/firewall/:id/persist",
|
||
|
|
validateHostId,
|
||
|
|
managerHandler(
|
||
|
|
runOnHost,
|
||
|
|
"execute",
|
||
|
|
"firewall_persist",
|
||
|
|
async (client, host) => {
|
||
|
|
const platform = await detectPlatform(client);
|
||
|
|
// Best-effort persistence across common tools.
|
||
|
|
const cmd =
|
||
|
|
"(command -v netfilter-persistent >/dev/null 2>&1 && netfilter-persistent save) || " +
|
||
|
|
"(command -v service >/dev/null 2>&1 && service iptables save) || " +
|
||
|
|
"(command -v nft >/dev/null 2>&1 && nft list ruleset > /etc/nftables.conf) || true";
|
||
|
|
const result = await execElevated(client, cmd, host.sudoPassword, {
|
||
|
|
forceSudo: true,
|
||
|
|
});
|
||
|
|
return {
|
||
|
|
success: result.code === 0,
|
||
|
|
output: result.stdout || result.stderr,
|
||
|
|
pkg: platform.pkg,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|