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
This commit is contained in:
parent
f78bc5adcb
commit
fe4ebb79b6
4 changed files with 203 additions and 1 deletions
13
README.md
13
README.md
|
|
@ -589,7 +589,18 @@ default, you must create it manually.
|
||||||
"dndChanged": true,
|
"dndChanged": true,
|
||||||
"gameModeChanged": true,
|
"gameModeChanged": true,
|
||||||
"kbLayoutChanged": true,
|
"kbLayoutChanged": true,
|
||||||
"numLockChanged": true
|
"numLockChanged": true,
|
||||||
|
"vpnChanged": true,
|
||||||
|
},
|
||||||
|
"vpn": {
|
||||||
|
"enabled": false,
|
||||||
|
"provider": [
|
||||||
|
{
|
||||||
|
"name": "wireguard",
|
||||||
|
"interface": "your-connection-name",
|
||||||
|
"displayName": "Wireguard (Your VPN)"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ JsonObject {
|
||||||
|
|
||||||
property Sizes sizes: Sizes {}
|
property Sizes sizes: Sizes {}
|
||||||
property Toasts toasts: Toasts {}
|
property Toasts toasts: Toasts {}
|
||||||
|
property Vpn vpn: Vpn {}
|
||||||
|
|
||||||
component Sizes: JsonObject {
|
component Sizes: JsonObject {
|
||||||
property int width: 430
|
property int width: 430
|
||||||
|
|
@ -22,5 +23,11 @@ JsonObject {
|
||||||
property bool capsLockChanged: true
|
property bool capsLockChanged: true
|
||||||
property bool numLockChanged: true
|
property bool numLockChanged: true
|
||||||
property bool kbLayoutChanged: true
|
property bool kbLayoutChanged: true
|
||||||
|
property bool vpnChanged: true
|
||||||
|
}
|
||||||
|
|
||||||
|
component Vpn: JsonObject {
|
||||||
|
property bool enabled: false
|
||||||
|
property list<var> provider: ["netbird"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,14 @@ StyledRect {
|
||||||
checked: Notifs.dnd
|
checked: Notifs.dnd
|
||||||
onClicked: Notifs.dnd = !Notifs.dnd
|
onClicked: Notifs.dnd = !Notifs.dnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toggle {
|
||||||
|
icon: "vpn_key"
|
||||||
|
checked: VPN.connected
|
||||||
|
enabled: !VPN.connecting
|
||||||
|
visible: VPN.enabled
|
||||||
|
onClicked: VPN.toggle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
176
services/VPN.qml
Normal file
176
services/VPN.qml
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue