pragma Singleton // The firewall, answered as "what can another machine reach?" // // Listing zones and services is what firewall-cmd already does. The question it // does not answer needs both halves at once: a port is reachable only when // something is listening on a network address AND the firewall permits it. // Either alone tells you nothing, which is how a tidy set of rules coexists // with an exposed database. // // Changes go through firewall-cmd, which is polkit-aware, so they prompt -- // through Panama's own prompt now. import Quickshell import Quickshell.Io import QtQuick Singleton { id: root readonly property string helperPath: Quickshell.shellDir + "/scripts/panama-firewall" property bool running: false property bool enabledAtBoot: false property bool available: false property string defaultZone: "" property var allZones: [] property var activeZones: ({}) property var zones: [] property var exposed: [] property var exposedDataStores: [] property int sshSessions: 0 property bool scanned: false property string lastError: "" readonly property bool busy: query.running || mutation.running readonly property var zone: root.zones.length > 0 ? root.zones[0] : null // The range Fedora Workstation opens by default, if this zone has it. Named // separately because it is the single rule that explains almost everything // on the exposed list. readonly property var openRanges: (root.zone?.ports ?? []).filter( spec => String(spec).indexOf("-") > 0) readonly property bool wideOpen: root.openRanges.length > 0 function serviceCount(): int { return (root.zone?.services ?? []).length; } function allowedByRange(entry: var): bool { return String(entry?.allowedBy ?? "").indexOf("range") >= 0; } // What closing the open range would cut off, by name, so the consequence is // stated before it happens rather than discovered afterwards. function rangeDependents(): var { return root.exposed.filter(entry => root.allowedByRange(entry)); } function refresh(): void { if (query.running) return; query.command = [root.helperPath, "snapshot"]; query.running = true; } function absorb(text: string): void { try { const parsed = JSON.parse(text); root.running = parsed.running === true; root.enabledAtBoot = parsed.enabledAtBoot === true; root.available = parsed.available === true; root.defaultZone = String(parsed.defaultZone ?? ""); root.allZones = Array.isArray(parsed.allZones) ? parsed.allZones : []; root.activeZones = parsed.activeZones ?? ({}); root.zones = Array.isArray(parsed.zones) ? parsed.zones : []; root.exposed = Array.isArray(parsed.exposed) ? parsed.exposed : []; root.exposedDataStores = Array.isArray(parsed.exposedDataStores) ? parsed.exposedDataStores : []; root.sshSessions = Number(parsed.sshSessions ?? 0); root.lastError = String(parsed.error ?? ""); } catch (error) { root.lastError = "Could not read the firewall's state."; console.warn("Firewall: could not parse helper output:", error); } root.scanned = true; } function run(arguments: var): void { if (mutation.running) return; root.lastError = ""; mutation.command = [root.helperPath].concat(arguments); mutation.running = true; } function removeService(name: string): void { root.run(["remove-service", name]); } function addService(name: string): void { root.run(["add-service", name]); } function removePort(spec: string): void { root.run(["remove-port", spec]); } function addPort(spec: string): void { root.run(["add-port", spec]); } function setZone(interfaceName: string, zoneName: string): void { root.run(["set-zone", interfaceName, zoneName]); } function setDefaultZone(zoneName: string): void { root.run(["set-default-zone", zoneName]); } Component.onCompleted: root.refresh() Process { id: query stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } Process { id: mutation stdout: StdioCollector { onStreamFinished: root.absorb(this.text) } stderr: StdioCollector { onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim() } } }