Own the network: details, VPN, enterprise Wi-Fi, and a firewall that can also allow

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-24 16:31:52 -04:00
parent aba2d16ffa
commit b30bf40407
29 changed files with 4452 additions and 241 deletions
@@ -57,6 +57,12 @@ Singleton {
return root.exposed.filter(entry => root.allowedByRange(entry));
}
// zone name -> the helper's zone-info shape, for the zone browser. Cached
// because browsing means asking about the same handful of zones as a chip
// row repaints, and each answer is two firewall-cmd calls.
property var zoneDetails: ({})
property var pendingZones: []
function refresh(): void {
if (query.running)
return;
@@ -64,6 +70,52 @@ Singleton {
query.running = true;
}
// What a zone allows, for reading before choosing one. Returns the cached
// answer, or null while the first one is on its way -- and asks for it, so
// a chip that binds to this fills itself in.
//
// Read-only, so it needs no authorization and never prompts: this is the
// difference between looking at a zone and moving an interface into it.
function zoneInfo(zoneName: string): var {
if (zoneName === "")
return null;
if (root.zoneDetails[zoneName] !== undefined)
return root.zoneDetails[zoneName];
root.requestZoneInfo(zoneName);
return null;
}
function requestZoneInfo(zoneName: string): void {
if (zoneName === "" || root.pendingZones.indexOf(zoneName) >= 0)
return;
root.pendingZones = root.pendingZones.concat([zoneName]);
root.drainZones();
}
function drainZones(): void {
if (zoneQuery.running || root.pendingZones.length === 0)
return;
zoneQuery.subject = root.pendingZones[0];
zoneQuery.command = [root.helperPath, "zone-info", zoneQuery.subject];
zoneQuery.running = true;
}
function absorbZone(zoneName: string, text: string): void {
try {
const parsed = JSON.parse(text);
// Reassigned rather than mutated: QML does not notice a property
// change made inside a var object.
const next = Object.assign({}, root.zoneDetails);
next[zoneName] = parsed;
root.zoneDetails = next;
if (String(parsed.error ?? "") !== "")
root.lastError = String(parsed.error);
} catch (error) {
root.lastError = "Could not read what that zone allows.";
console.warn("Firewall: could not parse zone-info output:", error);
}
}
function absorb(text: string): void {
try {
const parsed = JSON.parse(text);
@@ -90,6 +142,10 @@ Singleton {
if (mutation.running)
return;
root.lastError = "";
// Every mutation here can change what a zone allows or which zone an
// interface is in, so the browser's cached descriptions are dropped
// rather than left to describe the firewall as it used to be.
root.zoneDetails = ({});
mutation.command = [root.helperPath].concat(arguments);
mutation.running = true;
}
@@ -120,4 +176,31 @@ Singleton {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
}
// Kept out of `busy` on purpose: reading what a zone allows changes nothing,
// so it must not disable the buttons that do.
Process {
id: zoneQuery
property string subject: ""
stdout: StdioCollector {
onStreamFinished: root.absorbZone(zoneQuery.subject, this.text)
}
stderr: StdioCollector {
onStreamFinished: if (this.text.trim() !== "") root.lastError = this.text.trim()
}
onExited: {
root.pendingZones = root.pendingZones.filter(name => name !== zoneQuery.subject);
zoneDrain.restart();
}
}
// One tick later, because `running` has not gone false inside onExited and
// the queue would stall on its own guard.
Timer {
id: zoneDrain
interval: 0
onTriggered: root.drainZones()
}
}