// Firewall entfernter Hosts über SSH einsehen und bearbeiten: UFW sowie iptables/ip6tables (Tabelle filter). // Braucht root: entweder als root angemeldet oder sudo mit dem gespeicherten Host-Passwort (über stdin). // Alle Werte aus der Oberfläche werden streng geprüft, bevor sie in einen Shell-Befehl gelangen. const i18n = require('../i18n'); const { execOn } = require('./docker'); const lastLine = (s) => String(s || '').split('\n').map((l) => l.trim()).filter(Boolean).slice(-1)[0] || ''; const RX = { port: /^\d{1,5}([:,]\d{1,5})*$/, addr: /^(any|[0-9a-fA-F.:]+(\/\d{1,3})?)$/, comment: /^[^'"\\`$\n]{0,80}$/, chain: /^[A-Za-z0-9_.-]{1,40}$/, iface: /^[A-Za-z0-9_.@-]{1,15}\+?$/, }; const check = (v, rx, what) => { if (!rx.test(String(v))) throw new Error(i18n.t('Invalid value for {field}: {value}', { field: what, value: v })); return String(v); }; // UFW-Regeln aus `ufw status numbered` function parseUfw(out) { const status = /Status:\s*active/i.test(out) ? 'active' : 'inactive'; const rules = []; for (const line of out.split('\n')) { const m = line.match(/^\[\s*(\d+)\]\s+(.+?)\s{2,}(ALLOW|DENY|REJECT|LIMIT)(?:\s+(IN|OUT|FWD))?\s+(.+?)\s*$/); if (!m) continue; let [, num, to, action, dir, from] = m; let comment = ''; const c = from.match(/^(.*?)\s+#\s*(.*)$/); if (c) { from = c[1].trim(); comment = c[2]; } rules.push({ num: Number(num), to: to.trim(), action, dir: dir || 'IN', from: from.trim(), comment, v6: /\(v6\)/.test(to + from) }); } return { status, rules }; } // iptables -S: Richtlinien, Ketten und Regeln (Nummer = Position in der Kette) function parseIptables(out) { const chains = new Map(); const get = (n) => { if (!chains.has(n)) chains.set(n, { name: n, policy: null, rules: [] }); return chains.get(n); }; for (const line of out.split('\n')) { let m; if ((m = line.match(/^-P (\S+) (\S+)/))) get(m[1]).policy = m[2]; else if ((m = line.match(/^-N (\S+)/))) get(m[1]); else if ((m = line.match(/^-A (\S+) (.*)$/))) { const c = get(m[1]); c.rules.push({ num: c.rules.length + 1, spec: m[2] }); } } return [...chains.values()]; } class FirewallManager { constructor(ssh) { this.ssh = ssh; this.sessions = new Map(); // id -> { conn, jumps, host, sudo, tools } } get(id) { const s = this.sessions.get(id); if (!s) throw new Error(i18n.t('Firewall session not found')); return s; } // Befehl mit root-Rechten ausführen run(s, cmd) { if (!s.sudo) return execOn(s.conn, `PATH=$PATH:/usr/sbin:/sbin ${cmd}`); return execOn(s.conn, `sudo -S -p '' ${cmd}`, `${s.host.password || ''}\n`); } async runOk(s, cmd) { const r = await this.run(s, cmd); if (r.code) throw new Error(lastLine(r.err) || lastLine(r.out) || i18n.t('Command failed with code {code}', { code: r.code })); return r.out; } async open(id, host, onClose) { const { conn, jumps } = await this.ssh.connect(host, id); const s = { conn, jumps, host, sudo: false, tools: [] }; this.sessions.set(id, s); conn.on('close', () => { if (this.sessions.has(id)) { this.close(id); onClose(); } }); conn.on('error', () => {}); const uid = (await execOn(conn, 'id -u')).out.trim(); if (uid !== '0') { s.sudo = true; const r = await execOn(conn, "sudo -S -p '' -v", `${host.password || ''}\n`); if (r.code) throw new Error(i18n.t('Root privileges are required. Log in as root or save the password of a user with sudo rights on the host.')); } const t = await this.run(s, 'sh -c \'for c in ufw iptables ip6tables firewall-cmd; do command -v $c >/dev/null 2>&1 && echo $c; done; true\''); s.tools = t.out.split('\n').map((x) => x.trim()).filter(Boolean); if (!s.tools.includes('ufw') && !s.tools.includes('iptables')) { throw new Error(s.tools.includes('firewall-cmd') ? i18n.t('This host uses firewalld, which is not supported yet.') : i18n.t('Neither UFW nor iptables was found on this host.')); } return { tools: s.tools, sudo: s.sudo }; } async list(id, backend) { const s = this.get(id); if (backend === 'ufw') { const out = await this.runOk(s, 'ufw status numbered'); const verbose = await this.runOk(s, 'ufw status verbose'); const def = verbose.match(/Default:\s*(\w+)\s*\(incoming\),\s*(\w+)\s*\(outgoing\)(?:,\s*(\w+)\s*\(routed\))?/i); return { ...parseUfw(out), defaults: def ? { incoming: def[1], outgoing: def[2], routed: def[3] || '' } : null }; } const bin = backend === 'ip6tables' ? 'ip6tables' : 'iptables'; return { chains: parseIptables(await this.runOk(s, `${bin} -S`)) }; } async ufw(id, op, a = {}) { const s = this.get(id); switch (op) { case 'enable': return this.runOk(s, 'ufw --force enable'); case 'disable': return this.runOk(s, 'ufw disable'); case 'delete': return this.runOk(s, `ufw --force delete ${Number(a.num)}`); case 'default': { const pol = check(a.policy, /^(allow|deny|reject)$/, 'policy'); const dir = check(a.dir, /^(incoming|outgoing|routed)$/, 'direction'); return this.runOk(s, `ufw default ${pol} ${dir}`); } case 'add': { const action = check(a.action, /^(allow|deny|reject|limit)$/, 'action'); const dir = check(a.dir || 'in', /^(in|out)$/, 'direction'); const from = check(a.from || 'any', RX.addr, 'from'); const to = check(a.to || 'any', RX.addr, 'to'); const parts = ['ufw', a.top ? 'insert 1' : '', action, dir]; if (a.port) { check(a.port, RX.port, 'port'); const proto = check(a.proto || 'any', /^(any|tcp|udp)$/, 'protocol'); // Portbereiche/-listen verlangen bei UFW ein Protokoll if (/[:,]/.test(a.port) && proto === 'any') throw new Error(i18n.t('Port ranges and lists need a protocol (TCP or UDP).')); if (proto !== 'any') parts.push('proto', proto); parts.push('from', from, 'to', to, 'port', a.port); } else parts.push('from', from, 'to', to); if (a.comment) parts.push('comment', `'${check(a.comment, RX.comment, 'comment')}'`); return this.runOk(s, parts.filter(Boolean).join(' ')); } default: throw new Error('Invalid operation'); } } async ipt(id, op, a = {}) { const s = this.get(id); const bin = a.family === 6 ? 'ip6tables' : 'iptables'; switch (op) { case 'delete': return this.runOk(s, `${bin} -D ${check(a.chain, RX.chain, 'chain')} ${Number(a.num)}`); case 'policy': return this.runOk(s, `${bin} -P ${check(a.chain, /^(INPUT|OUTPUT|FORWARD)$/, 'chain')} ${check(a.policy, /^(ACCEPT|DROP)$/, 'policy')}`); case 'add': { const parts = [bin, a.top ? '-I' : '-A', check(a.chain, RX.chain, 'chain')]; const proto = check(a.proto || 'all', /^(all|tcp|udp|icmp|icmpv6)$/, 'protocol'); if (proto !== 'all') parts.push('-p', proto); if (a.source && a.source !== 'any') parts.push('-s', check(a.source, RX.addr, 'source')); if (a.iface) parts.push(a.chain === 'OUTPUT' ? '-o' : '-i', check(a.iface, RX.iface, 'interface')); if (a.port) { check(a.port, RX.port, 'port'); if (proto !== 'tcp' && proto !== 'udp') throw new Error(i18n.t('A port needs the protocol TCP or UDP.')); if (a.port.includes(',')) parts.push('-m', 'multiport', '--dports', a.port); else parts.push('--dport', a.port); } if (a.state) parts.push('-m', 'conntrack', '--ctstate', check(a.state, /^[A-Z,]+$/, 'state')); if (a.comment) parts.push('-m', 'comment', '--comment', `'${check(a.comment, RX.comment, 'comment')}'`); parts.push('-j', check(a.target, /^(ACCEPT|DROP|REJECT|LOG|RETURN)$/, 'target')); return this.runOk(s, parts.join(' ')); } case 'save': { // Dauerhaft speichern: Debian/Ubuntu (netfilter-persistent bzw. rules.v4/v6) oder Arch (iptables.rules) const script = 'if command -v netfilter-persistent >/dev/null 2>&1; then netfilter-persistent save; ' + 'elif [ -f /etc/debian_version ] && [ -d /etc/iptables ]; then iptables-save > /etc/iptables/rules.v4 && ip6tables-save > /etc/iptables/rules.v6; ' + 'elif [ -d /etc/iptables ]; then iptables-save > /etc/iptables/iptables.rules && ip6tables-save > /etc/iptables/ip6tables.rules; ' + 'else exit 3; fi'; const r = await this.run(s, `sh -c '${script}'`); if (r.code === 3) throw new Error(i18n.t('No known way to save iptables rules on this host (e.g. install iptables-persistent).')); if (r.code) throw new Error(lastLine(r.err) || i18n.t('Command failed with code {code}', { code: r.code })); return true; } default: throw new Error('Invalid operation'); } } close(id) { const s = this.sessions.get(id); if (!s) return; this.sessions.delete(id); try { s.conn.end(); } catch {} s.jumps?.forEach((c) => { try { c.end(); } catch {} }); } } module.exports = { FirewallManager, parseUfw, parseIptables };