import type { Client } from "ssh2"; import { execCommand } from "./common-utils.js"; import type { FirewallMetrics, FirewallChain, FirewallRule, } from "../../../types/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 = 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 { 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: [], }; } }