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
@@ -1,10 +1,18 @@
// Connections — wired, Wi-Fi, and Bluetooth.
// Connections — the whole network, not the half that was easy.
//
// Wi-Fi and Bluetooth are handled here rather than delegated. Everything goes
// through Quickshell.Networking and Quickshell.Bluetooth -- NetworkManager and
// BlueZ over DBus -- and nothing shells out to nmcli or bluetoothctl. That was
// the founding requirement for this desktop: never having to drop to a terminal
// to join a network.
// This page used to end in a card headed "Owned by Fedora" with two doors back
// to GNOME's panels: one for hidden and enterprise networks, one for VPN and
// proxies. Everything behind those doors now lives here, so the card is gone.
//
// Two mechanisms, deliberately kept apart:
//
// * Wi-Fi and Bluetooth state -- scanning, joining, pairing, the radio
// switches -- go through Quickshell.Networking and Quickshell.Bluetooth,
// which speak to NetworkManager and BlueZ over DBus. Connectivity.qml is
// pinned shell-out-free and stays that way.
// * Everything NetworkManager exposes only through nmcli -- per-connection
// addresses, forgetting a profile, MAC randomization, VPN import, hotspot,
// enterprise join -- goes through NetworkTools.qml and scripts/panama-network.
//
// Scanning follows this page being on screen. Wi-Fi scanning and especially
// Bluetooth discovery hold the radio, and doing either for a list nobody is
@@ -19,26 +27,78 @@ import qs.services
SettingsPage {
id: root
objectName: "connectivity"
title: "Connections"
lede: Connectivity.activeNetwork
? "Connected to " + Connectivity.activeNetwork.name
: "Wi-Fi, Bluetooth, and the things Fedora owns."
lede: {
const wifi = Connectivity.activeNetwork ? Connectivity.activeNetwork.name : "";
if (wifi !== "" && Connectivity.wiredOn)
return "On " + wifi + " and wired.";
if (wifi !== "")
return "On " + wifi + ".";
if (Connectivity.wiredOn)
return "Wired.";
return "Wi-Fi, Bluetooth, VPN, and what this machine can reach.";
}
// Drive the scanners only while this page is the one being shown.
// The wired connection's own drawer, and the two inline forms that are only
// open while someone is filling them in.
property bool wiredOpen: false
property bool hotspotOpen: false
property string hotspotName: ""
property bool importOpen: false
property string importPath: ""
readonly property string wiredConnection:
String(Connectivity.wiredDevice?.network?.name ?? "")
readonly property string activeWifi:
Connectivity.activeNetwork ? Connectivity.activeNetwork.name : ""
// Drive the scanners and the helper only while this page is the one being
// shown. Both cost radio time or nmcli invocations for a list nobody is
// reading.
Component.onCompleted: {
Connectivity.active = true;
NetworkTools.active = true;
if (!WifiShare.scanned)
WifiShare.refresh();
Vpn.refresh();
}
Component.onDestruction: Connectivity.active = false
Component.onDestruction: {
Connectivity.active = false;
NetworkTools.active = false;
}
// Addresses and the MAC in use both change with the connection, so the
// cached details for a network that just came up are stale the moment it
// does.
onActiveWifiChanged: {
if (root.activeWifi !== "")
NetworkTools.refreshDetails(root.activeWifi);
}
onWiredConnectionChanged: {
if (root.wiredConnection !== "")
NetworkTools.refreshDetails(root.wiredConnection);
}
TextRow {
visible: NetworkTools.lastError !== ""
label: "The network needs attention"
detail: NetworkTools.lastError
value: ""
divider: false
}
// ── Wired ────────────────────────────────────────────────────────────────
SettingsCard {
title: "Wired"
visible: Connectivity.wiredDevice !== null
SwitchRow {
label: "Ethernet"
SettingRow {
width: parent.width
label: root.wiredConnection !== "" ? root.wiredConnection : "Ethernet"
// Three states worth telling apart: on, off but plugged in, and
// nothing in the socket. "Not connected" covered all three and
// explained none of them.
@@ -54,13 +114,44 @@ SettingsPage {
// that blamed the hardware for what it had just done itself.
return device.name + " · off";
}
checked: Connectivity.wiredOn
enabled: Connectivity.wiredAvailable
controlWidth: 78
divider: false
onToggled: value => Connectivity.setWired(value)
activatable: root.wiredConnection !== "" && Connectivity.wiredOn
onActivated: root.wiredOpen = !root.wiredOpen
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 9
SettingsToggle {
anchors.verticalCenter: parent.verticalCenter
checked: Connectivity.wiredOn
enabled: Connectivity.wiredAvailable
onToggled: value => Connectivity.setWired(value)
}
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.wiredConnection !== "" && Connectivity.wiredOn
text: root.wiredOpen ? "▴" : "▾"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
ConnectionDetails {
width: parent.width
visible: root.wiredOpen && Connectivity.wiredOn
details: root.wiredConnection !== ""
? NetworkTools.detailsFor(root.wiredConnection) : null
}
}
// ── Wi-Fi ────────────────────────────────────────────────────────────────
SettingsCard {
title: "Wi-Fi"
// A Wi-Fi switch reading "On" above the words "No Wi-Fi adapter" is a
@@ -79,7 +170,7 @@ SettingsPage {
anchors.verticalCenter: parent.verticalCenter
checked: Connectivity.wifiEnabled
enabled: Connectivity.wifiAvailable
onToggled: value => Networking.wifiEnabled = value
onToggled: value => Connectivity.setWifiEnabled(value)
}
}
@@ -87,71 +178,188 @@ SettingsPage {
width: parent.width
visible: Connectivity.wifiEnabled
}
}
// Sharing a network by QR, the way GNOME's Wi-Fi panel does. The
// alternative is reading a passphrase out loud.
//
// The image holds the password in machine-readable form, so it is generated
// on demand rather than up front, and the helper writes it to tmpfs under
// XDG_RUNTIME_DIR instead of anywhere persistent.
SettingsCard {
visible: Connectivity.wifiDevice !== null && WifiShare.shareable.length > 0
title: "Share a network"
subtitle: WifiShare.sharing !== ""
? "Point a phone's camera at the code to join " + WifiShare.sharing + "."
: "Shows a QR code a phone can scan to join, without reading the password out."
// ── Hotspot ──────────────────────────────────────────────────────────
//
// One radio cannot be a client and an access point at the same time, so
// starting this drops whatever network the machine is on. Said before it
// happens rather than discovered when the browser stops loading.
Repeater {
model: WifiShare.shareable
ActionRow {
id: shareRow
required property var modelData
required property int index
label: shareRow.modelData.ssid
detail: WifiShare.sharing === shareRow.modelData.name
? "Showing a code below — anyone who can see the screen can join"
: "Saved network"
action: WifiShare.sharing === shareRow.modelData.name ? "Hide" : "Show code"
divider: shareRow.index < WifiShare.shareable.length - 1 || WifiShare.sharing !== ""
onTriggered: WifiShare.sharing === shareRow.modelData.name
? WifiShare.stopSharing()
: WifiShare.share(shareRow.modelData.name)
ActionRow {
width: parent.width
visible: Connectivity.wifiEnabled && !NetworkTools.hotspotActive
label: "Hotspot"
detail: "Share this machine's connection over Wi-Fi"
action: root.hotspotOpen ? "Cancel" : "Start hotspot…"
enabled: !NetworkTools.busy
divider: root.hotspotOpen
onTriggered: {
root.hotspotOpen = !root.hotspotOpen;
root.hotspotName = "";
}
}
// Drawn at its natural size on a white plate: a QR code inverted or
// tinted to match a dark theme is unreliable to scan, and this one has
// exactly one job.
Item {
Column {
width: parent.width
visible: WifiShare.sharing !== "" && WifiShare.imagePath !== ""
implicitHeight: visible ? plate.height + 20 : 0
visible: root.hotspotOpen && !NetworkTools.hotspotActive
Rectangle {
id: plate
anchors.horizontalCenter: parent.horizontalCenter
y: 10
width: 208
height: 208
radius: 10
color: "white"
TextFieldRow {
width: parent.width
label: "Network name"
detail: "What the hotspot calls itself to phones and laptops nearby"
placeholder: "panama-hotspot"
text: root.hotspotName
enabled: !NetworkTools.busy
onAccepted: value => root.hotspotName = value
}
Image {
anchors.centerIn: parent
width: 184
height: 184
smooth: false
fillMode: Image.PreserveAspectFit
cache: false
source: WifiShare.imagePath !== "" ? "file://" + WifiShare.imagePath : ""
ActionRow {
width: parent.width
label: "Start the hotspot"
detail: Connectivity.activeNetwork
? "This machine leaves " + Connectivity.activeNetwork.name
+ " while the hotspot runs — one radio cannot do both."
: "NetworkManager makes up a password and shows it once."
action: NetworkTools.busy ? "Starting…" : "Start"
enabled: root.hotspotName.trim() !== "" && !NetworkTools.busy
divider: false
onTriggered: {
NetworkTools.startHotspot(root.hotspotName.trim());
root.hotspotOpen = false;
}
}
}
TextRow {
width: parent.width
visible: NetworkTools.hotspotActive
label: "Hotspot is running"
detail: "Phones and laptops nearby can see this network and join it"
value: NetworkTools.hotspotSsid
}
// Shown once, on purpose. NetworkManager keeps the passphrase; this page
// never stores it, so leaving this screen means asking NetworkManager
// again rather than reading it back from Panama.
TextRow {
width: parent.width
visible: NetworkTools.hotspotActive && NetworkTools.hotspotPassword !== ""
label: "Password"
detail: "Shown once. Panama does not keep a copy — write it down or let someone type it in now."
value: NetworkTools.hotspotPassword
}
ActionRow {
width: parent.width
visible: NetworkTools.hotspotActive
label: "Stop the hotspot"
detail: "Anything connected through this machine loses its connection"
action: "Stop"
enabled: !NetworkTools.busy
divider: false
onTriggered: NetworkTools.stopHotspot()
}
}
// ── VPN ──────────────────────────────────────────────────────────────────
SettingsCard {
title: "VPN"
subtitle: "WireGuard and OpenVPN profiles NetworkManager holds for you."
TextRow {
width: parent.width
visible: Vpn.lastError !== ""
label: "The last VPN action did not finish"
detail: Vpn.lastError
value: ""
}
Repeater {
model: Vpn.connections
delegate: SwitchRow {
id: vpnRow
required property var modelData
required property int index
width: parent.width
label: String(vpnRow.modelData.name ?? "")
detail: String(vpnRow.modelData.kind ?? "VPN")
+ (vpnRow.modelData.active === true ? " · connected" : "")
checked: vpnRow.modelData.active === true
enabled: !Vpn.busy
onToggled: value => Vpn.setActive(String(vpnRow.modelData.uuid ?? ""), value)
}
}
TextRow {
width: parent.width
visible: Vpn.scanned && Vpn.connections.length === 0
label: "No VPNs configured"
detail: "Import a WireGuard or OpenVPN file to add one"
value: ""
}
// Names the profile that appeared rather than saying "done". An import
// that succeeds under a name you did not choose is otherwise invisible
// until you go looking for it in the list.
TextRow {
width: parent.width
visible: NetworkTools.lastImport !== ""
label: "Imported " + NetworkTools.lastImport
detail: "It is switched off until you turn it on above"
value: ""
}
ActionRow {
width: parent.width
label: "Import a VPN"
detail: "A .conf file from WireGuard, or a .ovpn file from OpenVPN"
action: root.importOpen ? "Cancel" : "Import…"
enabled: !NetworkTools.busy
divider: root.importOpen
onTriggered: {
root.importOpen = !root.importOpen;
root.importPath = "";
}
}
// A typed path rather than a file chooser this phase. A chooser is the
// better answer and is worth doing properly; a typed path is worth far
// more than the door back to GNOME it replaces.
Column {
width: parent.width
visible: root.importOpen
TextFieldRow {
width: parent.width
label: "File"
detail: "The full path to the profile you were given"
placeholder: "~/Downloads/work.ovpn"
text: root.importPath
enabled: !NetworkTools.busy
onAccepted: value => root.importPath = value
}
ActionRow {
width: parent.width
label: "Import this profile"
detail: "NetworkManager reads it and adds a profile. Nothing connects until you switch it on."
action: NetworkTools.busy ? "Importing…" : "Import"
enabled: root.importPath.trim() !== "" && !NetworkTools.busy
divider: false
onTriggered: {
NetworkTools.importVpn(root.importPath.trim());
root.importOpen = false;
}
}
}
}
// ── Bluetooth ────────────────────────────────────────────────────────────
SettingsCard {
title: "Bluetooth"
visible: Connectivity.adapter !== null
@@ -159,54 +367,119 @@ SettingsPage {
SettingRow {
label: "Bluetooth"
detail: Connectivity.adapter
? (Connectivity.adapter.enabled ? "On" : "Off")
detail: Connectivity.bluetoothAvailable
? (Connectivity.bluetoothEnabled ? "On" : "Off")
: "Unavailable"
controlWidth: 48
divider: !!(Connectivity.adapter && Connectivity.adapter.enabled)
divider: Connectivity.bluetoothEnabled
SettingsToggle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
checked: !!(Connectivity.adapter && Connectivity.adapter.enabled)
enabled: Connectivity.adapter !== null
onToggled: value => {
if (Connectivity.adapter)
Connectivity.adapter.enabled = value;
}
checked: Connectivity.bluetoothEnabled
enabled: Connectivity.bluetoothAvailable
onToggled: value => Connectivity.setBluetoothEnabled(value)
}
}
BluetoothPanel {
width: parent.width
visible: !!(Connectivity.adapter && Connectivity.adapter.enabled)
visible: Connectivity.bluetoothEnabled
}
}
// ── Radios and proxy ─────────────────────────────────────────────────────
SettingsCard {
title: "Owned by Fedora"
// These two panels drive NetworkManager over D-Bus, which is why they
// work in this session when most GNOME panels do not. They are split
// because GNOME splits them: "network" is wired, VPN and proxies, and
// does not contain Wi-Fi -- the one panel used to point everyone
// there, so the road to a hidden SSID or eduroam ended on a page
// without Wi-Fi on it.
subtitle: "Wired, VPN and Wi-Fi connection editing stay with GNOME's panels, which drive the same NetworkManager this page reads. Printers and online accounts have their own pages here."
title: "Radios and proxy"
subtitle: "The two settings that apply to every connection at once."
ActionRow {
label: "Wi-Fi networks"
detail: "Hidden networks, enterprise (802.1X) logins, and per-network settings"
action: "Open"
onTriggered: SystemSettings.openGnomePanel("wifi")
// A hardware kill switch cannot be overridden from software, so the
// switch says so rather than moving and having nothing happen.
SwitchRow {
width: parent.width
label: "Airplane mode"
detail: NetworkTools.airplaneHardBlocked
? "A switch on this machine is holding the radios off. Software cannot turn them back on."
: "Turns Wi-Fi and Bluetooth off together — the same switch the keyboard's airplane key throws"
checked: NetworkTools.airplaneOn
enabled: !NetworkTools.busy && !NetworkTools.airplaneHardBlocked
onToggled: value => NetworkTools.setAirplane(value)
}
ActionRow {
label: "Network connections"
detail: "VPN, proxies, and wired connection settings"
action: "Open"
OptionPickerRow {
width: parent.width
label: "Network proxy"
detail: NetworkTools.proxyMode === "none"
? "Applications that honour the system proxy use this. Not every application does."
: "In use: " + NetworkTools.proxySummary
enabled: !NetworkTools.busy
options: [
{
value: "none",
label: "Off",
detail: "Applications reach the network directly"
},
{
value: "manual",
label: "Manual",
detail: "A host and port you enter, used for http, https and socks alike"
},
{
value: "auto",
label: "Automatic (PAC)",
detail: "A configuration URL decides, per address"
}
]
current: NetworkTools.proxyMode
onPicked: value => NetworkTools.setProxyMode(String(value))
}
Column {
width: parent.width
visible: NetworkTools.proxyMode === "manual"
// Host and port are written together, because the proxy is only
// usable as a pair -- so each field commits with whatever the other
// one currently holds, and neither writes a half-configuration.
TextFieldRow {
width: parent.width
label: "Proxy host"
detail: "The machine applications should go through"
placeholder: "proxy.example.com"
text: NetworkTools.proxyHost
enabled: !NetworkTools.busy
onAccepted: value => {
if (value.trim() !== "" && NetworkTools.proxyPort !== "")
NetworkTools.setProxyManual(value.trim(), NetworkTools.proxyPort);
}
}
TextFieldRow {
width: parent.width
label: "Port"
detail: "The port that proxy listens on"
placeholder: "8080"
text: NetworkTools.proxyPort
enabled: !NetworkTools.busy
divider: false
onAccepted: value => {
if (value.trim() !== "" && NetworkTools.proxyHost !== "")
NetworkTools.setProxyManual(NetworkTools.proxyHost, value.trim());
}
}
}
TextFieldRow {
width: parent.width
visible: NetworkTools.proxyMode === "auto"
label: "Configuration URL"
detail: "The .pac file whoever runs the network published"
placeholder: "http://example.com/proxy.pac"
text: NetworkTools.proxyPac
enabled: !NetworkTools.busy
divider: false
onTriggered: SystemSettings.openGnomePanel("network")
onAccepted: value => NetworkTools.setProxyPac(value.trim())
}
}
}