mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 14:45:24 +00:00
255 lines
5.9 KiB
TypeScript
255 lines
5.9 KiB
TypeScript
import { execCommand } from "@termix/plugin-sdk/host-commands";
|
|
import type { Client } from "ssh2";
|
|
import type {
|
|
FirewallMetrics,
|
|
FirewallChain,
|
|
FirewallRule,
|
|
} from "../../shared/stats-widgets.js";
|
|
|
|
function parseIptablesRule(line: string): FirewallRule | null {
|
|
if (!line.startsWith("-A ")) return null;
|
|
|
|
const rule: FirewallRule = {
|
|
chain: "",
|
|
target: "",
|
|
protocol: "all",
|
|
source: "0.0.0.0/0",
|
|
destination: "0.0.0.0/0",
|
|
};
|
|
|
|
const chainMatch = line.match(/^-A\s+(\S+)/);
|
|
if (chainMatch) {
|
|
rule.chain = chainMatch[1];
|
|
}
|
|
|
|
const targetMatch = line.match(/-j\s+(\S+)/);
|
|
if (targetMatch) {
|
|
rule.target = targetMatch[1];
|
|
}
|
|
|
|
const protocolMatch = line.match(/-p\s+(\S+)/);
|
|
if (protocolMatch) {
|
|
rule.protocol = protocolMatch[1];
|
|
}
|
|
|
|
const sourceMatch = line.match(/-s\s+(\S+)/);
|
|
if (sourceMatch) {
|
|
rule.source = sourceMatch[1];
|
|
}
|
|
|
|
const destMatch = line.match(/-d\s+(\S+)/);
|
|
if (destMatch) {
|
|
rule.destination = destMatch[1];
|
|
}
|
|
|
|
const dportMatch = line.match(/--dport\s+(\S+)/);
|
|
if (dportMatch) {
|
|
rule.dport = dportMatch[1];
|
|
}
|
|
|
|
const sportMatch = line.match(/--sport\s+(\S+)/);
|
|
if (sportMatch) {
|
|
rule.sport = sportMatch[1];
|
|
}
|
|
|
|
const stateMatch = line.match(/--state\s+(\S+)/);
|
|
if (stateMatch) {
|
|
rule.state = stateMatch[1];
|
|
}
|
|
|
|
const interfaceMatch = line.match(/-i\s+(\S+)/);
|
|
if (interfaceMatch) {
|
|
rule.interface = interfaceMatch[1];
|
|
}
|
|
|
|
return rule;
|
|
}
|
|
|
|
function parseIptablesOutput(output: string): FirewallChain[] {
|
|
const chains: Map<string, FirewallChain> = new Map();
|
|
const lines = output.split("\n");
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
const policyMatch = trimmed.match(/^:(\S+)\s+(\S+)/);
|
|
if (policyMatch) {
|
|
const [, chainName, policy] = policyMatch;
|
|
chains.set(chainName, {
|
|
name: chainName,
|
|
policy: policy,
|
|
rules: [],
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const rule = parseIptablesRule(trimmed);
|
|
if (rule) {
|
|
let chain = chains.get(rule.chain);
|
|
if (!chain) {
|
|
chain = {
|
|
name: rule.chain,
|
|
policy: "ACCEPT",
|
|
rules: [],
|
|
};
|
|
chains.set(rule.chain, chain);
|
|
}
|
|
chain.rules.push(rule);
|
|
}
|
|
}
|
|
|
|
return Array.from(chains.values());
|
|
}
|
|
|
|
function parseNftablesOutput(output: string): FirewallChain[] {
|
|
const chains: FirewallChain[] = [];
|
|
let currentChain: FirewallChain | null = null;
|
|
|
|
const lines = output.split("\n");
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
|
|
const chainMatch = trimmed.match(
|
|
/chain\s+(\S+)\s*\{?\s*(?:type\s+\S+\s+hook\s+(\S+))?/,
|
|
);
|
|
if (chainMatch) {
|
|
if (currentChain) {
|
|
chains.push(currentChain);
|
|
}
|
|
currentChain = {
|
|
name: chainMatch[1].toUpperCase(),
|
|
policy: "ACCEPT",
|
|
rules: [],
|
|
};
|
|
continue;
|
|
}
|
|
|
|
if (currentChain && trimmed.startsWith("policy ")) {
|
|
const policyMatch = trimmed.match(/policy\s+(\S+)/);
|
|
if (policyMatch) {
|
|
currentChain.policy = policyMatch[1].toUpperCase();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (currentChain && trimmed && !trimmed.startsWith("}")) {
|
|
const rule: FirewallRule = {
|
|
chain: currentChain.name,
|
|
target: "",
|
|
protocol: "all",
|
|
source: "0.0.0.0/0",
|
|
destination: "0.0.0.0/0",
|
|
};
|
|
|
|
if (trimmed.includes("accept")) rule.target = "ACCEPT";
|
|
else if (trimmed.includes("drop")) rule.target = "DROP";
|
|
else if (trimmed.includes("reject")) rule.target = "REJECT";
|
|
|
|
const tcpMatch = trimmed.match(/tcp\s+dport\s+(\S+)/);
|
|
if (tcpMatch) {
|
|
rule.protocol = "tcp";
|
|
rule.dport = tcpMatch[1];
|
|
}
|
|
|
|
const udpMatch = trimmed.match(/udp\s+dport\s+(\S+)/);
|
|
if (udpMatch) {
|
|
rule.protocol = "udp";
|
|
rule.dport = udpMatch[1];
|
|
}
|
|
|
|
const saddrMatch = trimmed.match(/saddr\s+(\S+)/);
|
|
if (saddrMatch) {
|
|
rule.source = saddrMatch[1];
|
|
}
|
|
|
|
const daddrMatch = trimmed.match(/daddr\s+(\S+)/);
|
|
if (daddrMatch) {
|
|
rule.destination = daddrMatch[1];
|
|
}
|
|
|
|
const iifMatch = trimmed.match(/iif\s+"?(\S+)"?/);
|
|
if (iifMatch) {
|
|
rule.interface = iifMatch[1].replace(/"/g, "");
|
|
}
|
|
|
|
const ctStateMatch = trimmed.match(/ct\s+state\s+(\S+)/);
|
|
if (ctStateMatch) {
|
|
rule.state = ctStateMatch[1].toUpperCase();
|
|
}
|
|
|
|
if (rule.target) {
|
|
currentChain.rules.push(rule);
|
|
}
|
|
}
|
|
|
|
if (trimmed === "}") {
|
|
if (currentChain) {
|
|
chains.push(currentChain);
|
|
currentChain = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentChain) {
|
|
chains.push(currentChain);
|
|
}
|
|
|
|
return chains;
|
|
}
|
|
|
|
export async function collectFirewallMetrics(
|
|
client: Client,
|
|
): Promise<FirewallMetrics> {
|
|
try {
|
|
const iptablesResult = await execCommand(
|
|
client,
|
|
"iptables-save 2>/dev/null",
|
|
15000,
|
|
);
|
|
|
|
if (iptablesResult.stdout && iptablesResult.stdout.includes("*filter")) {
|
|
const chains = parseIptablesOutput(iptablesResult.stdout);
|
|
const hasRules = chains.some((c) => c.rules.length > 0);
|
|
|
|
return {
|
|
type: "iptables",
|
|
status: hasRules ? "active" : "inactive",
|
|
chains: chains.filter(
|
|
(c) =>
|
|
c.name === "INPUT" || c.name === "OUTPUT" || c.name === "FORWARD",
|
|
),
|
|
};
|
|
}
|
|
|
|
const nftResult = await execCommand(
|
|
client,
|
|
"nft list ruleset 2>/dev/null",
|
|
15000,
|
|
);
|
|
|
|
if (nftResult.stdout && nftResult.stdout.trim()) {
|
|
const chains = parseNftablesOutput(nftResult.stdout);
|
|
const hasRules = chains.some((c) => c.rules.length > 0);
|
|
|
|
return {
|
|
type: "nftables",
|
|
status: hasRules ? "active" : "inactive",
|
|
chains,
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: "none",
|
|
status: "unknown",
|
|
chains: [],
|
|
};
|
|
} catch {
|
|
return {
|
|
type: "none",
|
|
status: "unknown",
|
|
chains: [],
|
|
};
|
|
}
|
|
}
|