// The firewall, led by what another machine can actually reach. // // A rules list alone is not an answer: a port is reachable only when something // is listening on a network address AND the firewall permits it. On this // machine that crossing is the whole story -- the rules look unremarkable while // a database and a cache sit open, because Fedora Workstation's zone opens // every port above 1024 and rootless containers publish on all interfaces. // // Rich rules are shown but never edited. They are a syntax rather than a // setting, and a page that half-supports a syntax is a trap -- but hiding them // would mean the page misrepresents the configuration. import Quickshell import QtQuick import qs.config import qs.services SettingsPage { id: root objectName: "firewall" title: "Firewall" lede: "What another machine on your network can reach, and what allows it." property string confirmingRemoval: "" property bool confirmingRange: false Component.onCompleted: Firewall.refresh() TextRow { visible: Firewall.lastError !== "" label: "The firewall needs attention" detail: Firewall.lastError value: "" divider: false } // ── The finding, when there is one ─────────────────────────────────────── SettingsCard { visible: Firewall.exposedDataStores.length > 0 title: Firewall.exposedDataStores.length === 1 ? "A database is reachable from your network" : "Databases are reachable from your network" subtitle: { const names = Firewall.exposedDataStores.map(entry => String(entry.name)); return names.join(" and ") + " " + (names.length === 1 ? "is" : "are") + " listening on every interface, and this zone permits it. Anyone on your network can connect."; } Repeater { model: Firewall.exposedDataStores delegate: TextRow { required property var modelData required property int index width: parent.width label: String(modelData.name ?? "") detail: "Port " + modelData.port + "/" + String(modelData.protocol ?? "") + (String(modelData.process ?? "") !== "" ? " · " + String(modelData.process) : "") + " · allowed by " + String(modelData.allowedBy ?? "") value: "" divider: index < Firewall.exposedDataStores.length - 1 } } } // ── Everything reachable ───────────────────────────────────────────────── SettingsCard { title: "Reachable right now" subtitle: !Firewall.scanned ? "Checking what is listening and what the firewall permits…" : (Firewall.available ? "Listening on a network address, and permitted by the firewall. Both have to be true." : "The firewall is not running, so nothing here is being filtered.") Repeater { model: Firewall.exposed delegate: TextRow { required property var modelData required property int index width: parent.width label: String(modelData.name ?? "") detail: "Port " + modelData.port + "/" + String(modelData.protocol ?? "") + (String(modelData.process ?? "") !== "" && String(modelData.process) !== String(modelData.name) ? " · " + String(modelData.process) : "") + " · allowed by " + String(modelData.allowedBy ?? "") value: String(modelData.kind ?? "") === "data" ? "Database" : "" divider: index < Firewall.exposed.length - 1 } } TextRow { visible: Firewall.exposed.length === 0 && Firewall.scanned && Firewall.available label: "Nothing is reachable" detail: "No service is both listening on a network address and permitted" value: "" divider: false } } // ── The rules that allow it ────────────────────────────────────────────── SettingsCard { visible: Firewall.zone !== null title: "What this zone allows" subtitle: Firewall.zone ? String(Firewall.zone.name) + ", applied to " + (Firewall.zone.interfaces ?? []).join(" and ") : "" // The single rule that explains almost every row above. Column { width: parent.width visible: Firewall.wideOpen SettingRow { width: parent.width label: "Ports " + Firewall.openRanges.join(", ") detail: root.confirmingRange ? "Closing this cuts off " + Firewall.rangeDependents().length + " reachable service" + (Firewall.rangeDependents().length === 1 ? "" : "s") + ", including " + Firewall.rangeDependents().slice(0, 3) .map(entry => String(entry.name)).join(", ") + ". Anything that needs a port will have to be allowed by name." : "Fedora Workstation opens these so applications can listen without asking. It is why most of the list above is reachable." controlWidth: 230 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 SettingsButton { text: root.confirmingRange ? "Keep it open" : "Close the range…" enabled: !Firewall.busy onClicked: root.confirmingRange = !root.confirmingRange } SettingsButton { visible: root.confirmingRange text: "Close it" tone: "danger" enabled: !Firewall.busy onClicked: { root.confirmingRange = false; for (const spec of Firewall.openRanges) Firewall.removePort(String(spec)); } } } } } Repeater { model: Firewall.zone?.services ?? [] delegate: SettingRow { id: serviceRow required property var modelData required property int index readonly property string serviceName: String(serviceRow.modelData) readonly property bool confirming: root.confirmingRemoval === serviceRow.serviceName // Removing ssh while someone is connected over it ends their // session. Worth saying before, not after. readonly property bool risky: serviceRow.serviceName === "ssh" && Firewall.sshSessions > 0 width: parent.width label: serviceRow.serviceName detail: serviceRow.confirming ? (serviceRow.risky ? "Someone is connected over SSH right now. Removing this ends that session." : "Anything relying on this service stops being reachable.") : "Allowed by name, so it works whatever the port range says" controlWidth: 210 divider: serviceRow.index < (Firewall.zone?.services ?? []).length - 1 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 8 SettingsButton { text: serviceRow.confirming ? "Keep" : "Remove…" enabled: !Firewall.busy onClicked: root.confirmingRemoval = serviceRow.confirming ? "" : serviceRow.serviceName } SettingsButton { visible: serviceRow.confirming text: "Remove" tone: "danger" enabled: !Firewall.busy onClicked: { root.confirmingRemoval = ""; Firewall.removeService(serviceRow.serviceName); } } } } } // Shown, never edited. TextRow { visible: (Firewall.zone?.richRules ?? []).length > 0 label: "Rich rules" detail: "Custom rules in firewalld's own syntax. Shown here so this page does not misrepresent your configuration; edit them with firewall-cmd." value: (Firewall.zone?.richRules ?? []).length + " defined" divider: false } } // ── Zones ──────────────────────────────────────────────────────────────── SettingsCard { title: "Zones" subtitle: "A zone is a set of rules. Each network connection uses one." Repeater { model: Object.keys(Firewall.activeZones ?? ({})) delegate: TextRow { required property var modelData required property int index width: parent.width label: String(modelData) detail: "Applied to " + (Firewall.activeZones[String(modelData)] ?? []).join(", ") value: String(modelData) === Firewall.defaultZone ? "Default" : "" divider: true } } TextRow { label: "Default for new connections" detail: "Used when a network does not ask for a particular zone" value: Firewall.defaultZone divider: false } } // ── The service underneath ─────────────────────────────────────────────── SettingsCard { title: "Firewall service" TextRow { label: "firewalld" detail: !Firewall.scanned ? "Reading the firewall's state…" : (Firewall.running ? (Firewall.enabledAtBoot ? "Running, and starts with the system" : "Running, but not started at boot") : "Not running, so nothing is being filtered") value: !Firewall.scanned ? "Checking…" : (Firewall.running ? "Running" : "Stopped") divider: false } } }