1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-27 06:52:50 -05:00

net: switch to native VPN backend

This commit is contained in:
bbedward
2025-10-25 11:47:46 -04:00
parent 607b5320fd
commit 032777e32e
9 changed files with 234 additions and 304 deletions

View File

@@ -69,6 +69,24 @@ Singleton {
property string wifiPassword: ""
property string forgetSSID: ""
property var vpnProfiles: []
property var vpnActive: []
property bool vpnAvailable: false
property bool vpnIsBusy: false
property alias profiles: root.vpnProfiles
property alias activeConnections: root.vpnActive
property var activeUuids: vpnActive.map(v => v.uuid).filter(u => !!u)
property var activeNames: vpnActive.map(v => v.name).filter(n => !!n)
property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : ""
property string activeName: activeNames.length > 0 ? activeNames[0] : ""
property string activeDevice: vpnActive.length > 0 ? (vpnActive[0].device || "") : ""
property string activeState: vpnActive.length > 0 ? (vpnActive[0].state || "") : ""
property bool vpnConnected: activeUuids.length > 0
property alias available: root.vpnAvailable
property alias isBusy: root.vpnIsBusy
property alias connected: root.vpnConnected
property string networkInfoSSID: ""
property string networkInfoDetails: ""
property bool networkInfoLoading: false
@@ -94,7 +112,7 @@ Singleton {
signal networksUpdated
signal connectionChanged
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason)
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason, string connType, string connName, string vpnService)
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
@@ -164,7 +182,11 @@ Singleton {
credentialsReason = data.reason || "Credentials required"
credentialsRequested = true
credentialsNeeded(credentialsToken, credentialsSSID, credentialsSetting, credentialsFields, credentialsHints, credentialsReason)
const connType = data.connType || ""
const connName = data.name || data.connectionId || ""
const vpnService = data.vpnService || ""
credentialsNeeded(credentialsToken, credentialsSSID, credentialsSetting, credentialsFields, credentialsHints, credentialsReason, connType, connName, vpnService)
}
function addRef() {
@@ -202,6 +224,7 @@ Singleton {
const previousConnectingSSID = connectingSSID
backend = state.backend || ""
vpnAvailable = networkAvailable && backend === "networkmanager"
networkStatus = state.networkStatus || "disconnected"
primaryConnection = state.primaryConnection || ""
@@ -244,6 +267,12 @@ Singleton {
networksUpdated()
}
if (state.vpnProfiles) {
vpnProfiles = state.vpnProfiles
}
vpnActive = state.vpnActive || []
userPreference = state.preference || "auto"
isConnecting = state.isConnecting || false
connectingSSID = state.connectingSSID || ""
@@ -677,6 +706,122 @@ Singleton {
}
}
function refreshVpnProfiles() {
if (!vpnAvailable) return
DMSService.sendRequest("network.vpn.profiles", null, response => {
if (response.result) {
vpnProfiles = response.result
}
})
}
function refreshVpnActive() {
if (!vpnAvailable) return
DMSService.sendRequest("network.vpn.active", null, response => {
if (response.result) {
vpnActive = response.result
}
})
}
function connectVpn(uuidOrName, singleActive = false) {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
const params = {
uuidOrName: uuidOrName,
singleActive: singleActive
}
DMSService.sendRequest("network.vpn.connect", params, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to connect VPN"))
} else {
Qt.callLater(() => getState())
}
})
}
function connect(uuidOrName, singleActive = false) {
connectVpn(uuidOrName, singleActive)
}
function disconnectVpn(uuidOrName) {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
const params = {
uuidOrName: uuidOrName
}
DMSService.sendRequest("network.vpn.disconnect", params, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to disconnect VPN"))
} else {
Qt.callLater(() => getState())
}
})
}
function disconnect(uuidOrName) {
disconnectVpn(uuidOrName)
}
function disconnectAllVpns() {
if (!vpnAvailable || vpnIsBusy) return
vpnIsBusy = true
DMSService.sendRequest("network.vpn.disconnectAll", null, response => {
vpnIsBusy = false
if (response.error) {
ToastService.showError(I18n.tr("Failed to disconnect VPNs"))
} else {
Qt.callLater(() => getState())
}
})
}
function disconnectAllActive() {
disconnectAllVpns()
}
function toggleVpn(uuid) {
if (uuid) {
if (isActiveVpnUuid(uuid)) {
disconnectVpn(uuid)
} else {
connectVpn(uuid)
}
return
}
if (vpnProfiles.length > 0) {
connectVpn(vpnProfiles[0].uuid)
}
}
function toggle(uuid) {
toggleVpn(uuid)
}
function isActiveVpnUuid(uuid) {
return activeUuids && activeUuids.indexOf(uuid) !== -1
}
function isActiveUuid(uuid) {
return isActiveVpnUuid(uuid)
}
function refreshNetworkState() {
if (networkAvailable) {
getState()

View File

@@ -80,7 +80,7 @@ Singleton {
signal networksUpdated
signal connectionChanged
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason)
signal credentialsNeeded(string token, string ssid, string setting, var fields, var hints, string reason, string connType, string connName, string vpnService)
property bool usingLegacy: false
property var activeService: null

View File

@@ -1,246 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
// Minimal VPN controller backed by NetworkManager (nmcli + D-Bus monitor)
Singleton {
id: root
property int refCount: 0
onRefCountChanged: {
console.log("VpnService: refCount changed to", refCount)
if (refCount > 0 && !nmMonitor.running) {
console.log("VpnService: Starting nmMonitor")
nmMonitor.running = true
refreshAll()
} else if (refCount === 0 && nmMonitor.running) {
console.log("VpnService: Stopping nmMonitor")
nmMonitor.running = false
}
}
// State
property bool available: true
property bool isBusy: false
property string errorMessage: ""
// Profiles discovered on the system
// [{ name, uuid, type }]
property var profiles: []
// Allow multiple active VPNs (set true to allow concurrent connections)
// Default: allow multiple, to align with NetworkManager capability
property bool singleActive: false
// Active VPN connections (may be multiple)
// Full list and convenience projections
property var activeConnections: [] // [{ name, uuid, device, state }]
property var activeUuids: []
property var activeNames: []
// Back-compat single values (first active if present)
property string activeUuid: activeUuids.length > 0 ? activeUuids[0] : ""
property string activeName: activeNames.length > 0 ? activeNames[0] : ""
property string activeDevice: activeConnections.length > 0 ? (activeConnections[0].device || "") : ""
property string activeState: activeConnections.length > 0 ? (activeConnections[0].state || "") : ""
property bool connected: activeUuids.length > 0
// Use implicit property notify signals (profilesChanged, activeUuidChanged, etc.)
function refreshAll() {
listProfiles()
refreshActive()
}
// Monitor NetworkManager changes and refresh on activity
Process {
id: nmMonitor
command: ["gdbus", "monitor", "--system", "--dest", "org.freedesktop.NetworkManager"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
if (line.includes("ActiveConnection") || line.includes("PropertiesChanged") || line.includes("StateChanged")) {
refreshAll()
}
}
}
}
// Query all VPN profiles
function listProfiles() {
getProfiles.running = true
}
Process {
id: getProfiles
command: ["bash", "-lc", "nmcli -t -f NAME,UUID,TYPE connection show | while IFS=: read -r name uuid type; do case \"$type\" in vpn) svc=$(nmcli -g vpn.service-type connection show uuid \"$uuid\" 2>/dev/null); echo \"$name:$uuid:$type:$svc\" ;; wireguard) echo \"$name:$uuid:$type:\" ;; *) : ;; esac; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().length ? text.trim().split('\n') : []
const out = []
for (const line of lines) {
const parts = line.split(':')
if (parts.length >= 3 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
const svc = parts.length >= 4 ? parts[3] : ""
out.push({ name: parts[0], uuid: parts[1], type: parts[2], serviceType: svc })
}
}
root.profiles = out
}
}
}
// Query active VPN connection
function refreshActive() {
getActive.running = true
}
Process {
id: getActive
command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE,STATE", "connection", "show", "--active"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().length ? text.trim().split('\n') : []
let act = []
for (const line of lines) {
const parts = line.split(':')
if (parts.length >= 5 && (parts[2] === "vpn" || parts[2] === "wireguard")) {
act.push({ name: parts[0], uuid: parts[1], device: parts[3], state: parts[4] })
}
}
root.activeConnections = act
root.activeUuids = act.map(a => a.uuid).filter(u => !!u)
root.activeNames = act.map(a => a.name).filter(n => !!n)
}
}
}
function isActiveUuid(uuid) {
return root.activeUuids && root.activeUuids.indexOf(uuid) !== -1
}
function _looksLikeUuid(s) {
// Very loose check for UUID pattern
return s && s.indexOf('-') !== -1 && s.length >= 8
}
function connect(uuidOrName) {
if (root.isBusy) return
root.isBusy = true
root.errorMessage = ""
if (root.singleActive) {
// Bring down all active VPNs, then bring up the requested one
const isUuid = _looksLikeUuid(uuidOrName)
const escaped = ('' + uuidOrName).replace(/'/g, "'\\''")
const upCmd = isUuid ? `nmcli connection up uuid '${escaped}'` : `nmcli connection up id '${escaped}'`
const script = `set -e\n` +
`nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done\n` +
upCmd + `\n`
vpnSwitch.command = ["bash", "-lc", script]
vpnSwitch.running = true
} else {
if (_looksLikeUuid(uuidOrName)) {
vpnUp.command = ["nmcli", "connection", "up", "uuid", uuidOrName]
} else {
vpnUp.command = ["nmcli", "connection", "up", "id", uuidOrName]
}
vpnUp.running = true
}
}
function disconnect(uuidOrName) {
if (root.isBusy) return
root.isBusy = true
root.errorMessage = ""
if (_looksLikeUuid(uuidOrName)) {
vpnDown.command = ["nmcli", "connection", "down", "uuid", uuidOrName]
} else {
vpnDown.command = ["nmcli", "connection", "down", "id", uuidOrName]
}
vpnDown.running = true
}
function toggle(uuid) {
if (uuid) {
if (isActiveUuid(uuid)) disconnect(uuid)
else connect(uuid)
return
}
if (root.profiles.length > 0) {
connect(root.profiles[0].uuid)
}
}
Process {
id: vpnUp
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
if (!text.toLowerCase().includes("successfully")) {
root.errorMessage = text.trim()
}
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to connect VPN"
}
}
}
Process {
id: vpnDown
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
if (!text.toLowerCase().includes("deactivated") && !text.toLowerCase().includes("successfully")) {
root.errorMessage = text.trim()
}
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to disconnect VPN"
}
}
}
function disconnectAllActive() {
if (root.isBusy) return
root.isBusy = true
const script = `nmcli -t -f UUID,TYPE connection show --active | awk -F: '$2 ~ /^(vpn|wireguard)$/ {print $1}' | while read u; do [ -n \"$u\" ] && nmcli connection down uuid \"$u\" || true; done`
vpnSwitch.command = ["bash", "-lc", script]
vpnSwitch.running = true
}
// Sequenced down/up using a single shell for exclusive switch
Process {
id: vpnSwitch
running: false
stdout: StdioCollector {
onStreamFinished: {
root.isBusy = false
refreshAll()
}
}
onExited: exitCode => {
root.isBusy = false
if (exitCode !== 0 && root.errorMessage === "") {
root.errorMessage = "Failed to switch VPN"
}
}
}
}