diff options
| author | Robin Seger <pixelkhaos@gmail.com> | 2025-10-14 07:05:15 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-14 16:05:15 +1100 |
| commit | fe4ebb79b6162d7e5e4e9a00d8a39ff10876fb8c (patch) | |
| tree | f77520d936b845bd27e47809e941eb83f46187db /services | |
| parent | internal: fix lsp warnings (diff) | |
| download | caelestia-shell-fe4ebb79b6162d7e5e4e9a00d8a39ff10876fb8c.tar.gz caelestia-shell-fe4ebb79b6162d7e5e4e9a00d8a39ff10876fb8c.tar.bz2 caelestia-shell-fe4ebb79b6162d7e5e4e9a00d8a39ff10876fb8c.zip | |
feat: VPN toggle (#689)
* feat: configurable VPN toggle for Wireguard
- Added VPN service for wg-quick
- Added VPN toggle to utilities quick toggles
- Configuration in UtilitiesConfig (enabled, connectionName)
* fix: monitoring and toasts
- Using nmcli monitor for state detection instead of polling
- Added VPN toast notifications
* fix: use polkit
* feat: multi-provider VPN support
- Added support for netbird and tailscale providers
- Universal interface detection using ip link show
- Provider-specific privilege handling (pkexec only for wireguard)
- Updated README with VPN configuration examples
* feat: less hardcoded, configurable providers
* removed comments
* code style changes
* reorganize signal handler
Diffstat (limited to 'services')
| -rw-r--r-- | services/VPN.qml | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/services/VPN.qml b/services/VPN.qml new file mode 100644 index 0000000..10e5e7e --- /dev/null +++ b/services/VPN.qml @@ -0,0 +1,176 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick +import qs.config +import Caelestia + +Singleton { + id: root + + property bool connected: false + + readonly property bool connecting: connectProc.running || disconnectProc.running + readonly property bool enabled: Config.utilities.vpn.enabled + readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard" + readonly property bool isCustomProvider: typeof providerInput === "object" + readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) + readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" + readonly property var currentConfig: { + const name = providerName; + const iface = interfaceName; + const defaults = getBuiltinDefaults(name, iface); + + if (isCustomProvider) { + const custom = providerInput; + return { + connectCmd: custom.connectCmd || defaults.connectCmd, + disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, + interface: custom.interface || defaults.interface, + displayName: custom.displayName || defaults.displayName + }; + } + + return defaults; + } + + function getBuiltinDefaults(name, iface) { + const builtins = { + "wireguard": { + connectCmd: ["pkexec", "wg-quick", "up", iface], + disconnectCmd: ["pkexec", "wg-quick", "down", iface], + interface: iface, + displayName: iface + }, + "warp": { + connectCmd: ["warp-cli", "connect"], + disconnectCmd: ["warp-cli", "disconnect"], + interface: "CloudflareWARP", + displayName: "Warp" + }, + "netbird": { + connectCmd: ["netbird", "up"], + disconnectCmd: ["netbird", "down"], + interface: "wt0", + displayName: "NetBird" + }, + "tailscale": { + connectCmd: ["tailscale", "up"], + disconnectCmd: ["tailscale", "down"], + interface: "tailscale0", + displayName: "Tailscale" + } + }; + + return builtins[name] || { + connectCmd: [name, "up"], + disconnectCmd: [name, "down"], + interface: iface || name, + displayName: name + }; + } + + function connect(): void { + if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { + connectProc.exec(root.currentConfig.connectCmd); + } + } + + function disconnect(): void { + if (connected && !connecting && root.currentConfig && root.currentConfig.disconnectCmd) { + disconnectProc.exec(root.currentConfig.disconnectCmd); + } + } + + function toggle(): void { + if (connected) { + disconnect(); + } else { + connect(); + } + } + + function checkStatus(): void { + if (root.enabled) { + statusProc.running = true; + } + } + + onConnectedChanged: { + if (!Config.utilities.toasts.vpnChanged) + return; + + const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; + if (connected) { + Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); + } else { + Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + } + } + + Component.onCompleted: root.enabled && statusCheckTimer.start() + + Process { + id: nmMonitor + + running: root.enabled + command: ["nmcli", "monitor"] + stdout: SplitParser { + onRead: statusCheckTimer.restart() + } + } + + Process { + id: statusProc + + command: ["ip", "link", "show"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { + const iface = root.currentConfig ? root.currentConfig.interface : ""; + root.connected = iface && text.includes(iface + ":"); + } + } + } + + Process { + id: connectProc + + onExited: statusCheckTimer.start() + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && !error.includes("[#]") && !error.includes("already exists")) { + console.warn("VPN connection error:", error); + } else if (error.includes("already exists")) { + root.connected = true; + } + } + } + } + + Process { + id: disconnectProc + + onExited: statusCheckTimer.start() + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && !error.includes("[#]")) { + console.warn("VPN disconnection error:", error); + } + } + } + } + + Timer { + id: statusCheckTimer + + interval: 500 + onTriggered: root.checkStatus() + } +} |