|
by Cuig
Why write this script at all?
Because I like simple, non-intrusive ways of getting things done and, for various reasons, other options open to me were not acceptable.
Main amongst those is that Proton, my selected VPN provider, only provides their application for distros that have systemd running. There is a very short list of distros they “support”.
An alternative called wireguird (note spelling), a nice GUI to manage wireguard VPNs, has had a problem on my installs, in that when a VPN is deactivated it leaves me without an internet connection!
So that is my reason for this script. But why a VPN at all?
Why do I want to use a VPN? Several reasons really, most of which have been covered in other pages of this magazine, but mostly because of my government’s actions.
- Lots of foreign sites (example: rt.com) are blocked in my country in an attempt to allow me to read ONLY what my government approves.
- That is not acceptable to me under any circumstances.
- That and the added privacy of using a VPN is why I use one.
- I might occasionally view public service (free-to-air) TV services from other locales, which require use of a VPN.
I use the wireguard protocol as it is the most recent, secure and fastest of those generally available. I use SUDO in this script as the wireguard.conf files are stored in /etc/wireguard and are owned by root and some of the commands used need root privileges. If the permissions of these wireguard files are wrong, an error is thrown - presumably because this would reduce the security.
I have used Zenity for the GUI as there was something I wanted to do that I could not figure out in Yad (which I tried first).
Be aware that SUDO needs to be set up for the various elements used in this script. Besides doing that, the dependencies are Zenity, SUDO, wireguard-tools, and in my case for the icon, papirus-icon-theme.
The Script
The script will select your primary network connection and only permit one VPN connection to be active at any time.
On launch with no VPN active, this is shown. It lists the files with “.conf” extension in the /etc/wireguard directory. These are the files one gets from the VPN service provider. Yours of course will be different to mine shown below.

As can be seen from the “Disconnected” at the top, there is no VPN active and you are invited to select one to activate. Having made your selection, simply click the “Activate” button and the VPN is activated.
A window is shown to confirm which VPN is activated. Select OK to close this.

On the next launch of the script the active VPN will be shown under the title.

From this window you can either Deactivate the VPN, returning you to your normal IP address, or you can select another VPN file and click Activate.
Selecting a new file and the “Activate” button will Deactivate the existing VPN and Activate the newly chosen one, ensuring you have only one VPN active at any time. A small window will inform you of the name of the new VPN you chose.
Selecting the Deactivate button will bring up the following window.

On the main window, the “Close” button is obvious, but the “Cleanup” button deserves some explanation. In case of some mishap, such as a PC freeze or hard shutdown, using the “Cleanup” button should get things back to normal. This option was added to help resolve some fault or failure. It essentially resets your connection back to default. It could happen that your desktop freezes due to some other application or process going wrong and your VPN network is left in a confused state. This should help if this should occur.
I use Proton VPN, so I used their icon for the script. The script is easily editable to change the icon – just one entry to be changed.
There is not much else to say really. This activates, switches and deactivates VPN connections using a simple interface. Nothing fancy; just functional.
I included a desktop file for convenience. I placed the working script in $HOME/bin/ProtonWG, so if you place it elsewhere, the path in the desktop file will need to be edited.
First here is the .desktop file, my-vpn-manager.desktop:
#!/usr/bin/env xdg-open
[Desktop Entry]
Categories=Internet;Network;
Comment[en_GB]=VPN Manager
Comment=VPN Manager
Exec=$HOME/bin/ProtonWG/my-vpn-manager
GenericName[en_GB]=VPN Manager
GenericName=VPN Manager
Icon=/usr/share/icons/Papirus/48x48/apps/proton-vpn-logo.svg
MimeType=
Name[en_GB]=my-vpn-manager
Name=my-vpn-manager
Path=
StartupNotify=true
Terminal=false
TerminalOptions=
Type=Application
X-KDE-SubstituteUID=false
X-KDE-Username=
Next is the working script, my-vpn-manager.
- #!/bin/bash
- # ======================================================
- # VPN Manager Script v1.6 2026-04-13
- # Features: Switching VPN, Stale VPN cleanup, File detection of “.conf” only
- # OS: PCLinuxOS 2026
- # Dependencies: Sudo, wireguard-tools, Zenity, papirus-icon-theme
- # ======================================================
- set -x # For debug output in a terminal
- cd /etc/wireguard
- Icon=/usr/share/icons/Papirus/48x48/apps/proton-vpn-logo.svg
- # --- Helper: Detect Active VPN ---
- get_active_vpn() {
- # Must use sudo to read kernel state. We need the 2nd field.
- local iface=$(sudo wg show 2>/dev/null | awk 'NR==1 {print $2}')
- echo "$iface"
- }
- # --- Helper: Find Primary Network Interface ---
- get_primary_iface() {
- # Try to find the interface with the default route
- local iface=$(ip route | grep default | awk '{print $5}' | head -n1)
- # Fallback: First non-loopback interface
- if [[ -z "$iface" ]]; then
- iface=$(ip link show 2>/dev/null | awk -F': ' '/^[0-9]+: (eth|wl|en)/ {print $2}' | head -n1)
- fi
- # Last resort: eth0
- if [[ -z "$iface" ]]; then
- iface="eth0"
- fi
- echo "$iface"
- }
- # --- Helper: Cleanup ALL Stale Connections ---
- do_cleanup() {
- echo ">>> Running Cleanup..."
- # Extract ONLY the interface names (first column of first line for each block)
- # Use 'awk' to find lines starting with "interface:" and print the 2nd field.
- local ifaces=$(sudo wg show 2>/dev/null | awk '/^interface:/ {print $2}')
- if [[ -z "$ifaces" ]]; then
- zenity --window-icon=$Icon --info --text="
No active connections found."
- return
- fi
- local count=0
- for i in $ifaces; do
- echo "Stopping: $i"
- sudo wg-quick down "$i" 2>/dev/null || true
- ((count++))
- done
- local PRIMARY=$(get_primary_iface)
- echo "Resetting network interface: $PRIMARY"
- if [ -f /usr/libexec/nm-ifdown ] && [ -f /usr/libexec/nm-ifup ]; then
- sudo /usr/libexec/nm-ifdown "$PRIMARY"; sleep 1; sudo /usr/libexec/nm-ifup "$PRIMARY"
- else
- sudo ip link set "$PRIMARY" down; sleep 1; sudo ip link set "$PRIMARY" up
- fi
- zenity --window-icon=$Icon --info --text="
Cleaned up $count connection(s)."
- sleep 1
- }
- # --- Helper: Hard Kill ---
- do_hard_kill() {
- local iface=$1
- sudo wg-quick down "$iface"
- local PRIMARY=$(get_primary_iface)
- if [ -f /usr/libexec/nm-ifdown ] && [ -f /usr/libexec/nm-ifup ]; then
- sudo /usr/libexec/nm-ifdown "$PRIMARY"; sleep 1; sudo /usr/libexec/nm-ifup "$PRIMARY"
- else
- sudo ip link set "$PRIMARY" down; sleep 1; sudo ip link set "$PRIMARY" up
- fi
- }
- # ========================================================
- # MAIN LOOP
- # ========================================================
- while true; do
- # 1. Check Status (Refreshed every loop)
- CURRENT=$(get_active_vpn)
- if [[ -n "$CURRENT" ]]; then
- TITLE="
SELECT A VPN TO ACTIVATE - ACTIVE: $CURRENT"
- else
- TITLE="
SELECT A VPN TO ACTIVATE - DISCONNECTED"
- fi
- # 2. Show Menu
- SELECTED=$(ls *.conf 2>/dev/null | zenity --window-icon=$Icon --text="$TITLE" --list --height 600 --column="File Name" --ok-label="Activate" --extra-button="Deactivate" --extra-button="Cleanup" --cancel-label="Close")
- EXIT_CODE=$?
- # 3. Cancel
- if [ $EXIT_CODE -eq 1 ] && [ -z "$SELECTED" ]; then
- exit 0
- fi
- # 4. Cleanup Button
- if [ "$SELECTED" = "Cleanup" ]; then
- do_cleanup
- continue
- fi
- # 5. Deactivate Button
- if [ "$SELECTED" = "Deactivate" ]; then
- if [[ -n "$CURRENT" ]]; then
- do_hard_kill "$CURRENT"
- zenity --window-icon=$Icon --info --text="
$CURRENT deactivated."
- else
- zenity --window-icon=$Icon --info --text="
No VPN active."
- fi
- continue
- fi
- # 6. Handle Activate Button - new or switch VPN
- if [ $EXIT_CODE -eq 0 ]; then
- if [ -z "$SELECTED" ]; then
- zenity --window-icon=$Icon --warning --text="
No file selected."
- continue
- fi
- # If a VPN is active, stop it silently first
- if [[ -n "$CURRENT" ]]; then
- echo "Switching from $CURRENT to $SELECTED..."
- sudo wg-quick down "$CURRENT"
- # Wait for kernel to fully clear routes (Critical for stability)
- sleep 2
- fi
- break
- fi
- done
- # --- Final Activation ---
- echo "🚀 Activating: $SELECTED"
- FILE="${SELECTED%.*}"
- # Safety Check: Ensure no other VPN is active before starting
- sleep 1
- FINAL_CHECK=$(get_active_vpn)
- if [[ -n "$FINAL_CHECK" ]]; then
- # If the same interface is still there, try one last time to stop it
- if [[ "$FINAL_CHECK" == "$CURRENT" ]]; then
- echo "Warning: Interface $FINAL_CHECK still active. Retrying stop..."
- sudo wg-quick down "$FINAL_CHECK"
- sleep 1
- FINAL_CHECK=$(get_active_vpn)
- fi
- if [[ -n "$FINAL_CHECK" ]]; then
- zenity --window-icon=$Icon --error --text="
Failed: Another VPN ($FINAL_CHECK) is still active."
- exit 1
- fi
- fi
- sudo wg-quick up "$FILE"
- if [ $? -eq 0 ]; then
- zenity --window-icon=$Icon --info --text="
VPN $FILE activated successfully."
- else
- zenity --window-icon=$Icon --error --text="
Failed to activate $FILE."
- fi
You can download the bash script from the magazine server, here. Store the file in the directory where you normally store your bash scripts. Be sure to change the filename from wireguard-vpn-manager.sh.txt to wireguard-vpn-manager.sh, and to make the file executable. The bash script is 5.2 KiB in size, so it should download quite quickly.
A Note About SUDO
One aspect of this exercise I had forgotten about was assigning necessary sudo privileges to the user. This is done by a couple of simple edits to the /etc/sudoers file.
As we know, to gain full privileges we su to root, and this is achieved by this entry in the file: root ALL=(ALL:ALL) ALL
So if we want our user to have similar privileges we add this entry: user ALL=(ALL:ALL) ALL
Because we are using sudo within a script we have no means of knowing when it asks for a password, so we specify that sudo does not require a password for that user: user ALL=(ALL:ALL) NOPASSWD: ALL
This has the effect of sudo never requiring a password from the 'user' specified. Other users will still need to enter their password.
For more specific control over what commands to allow without password, the sudoers entry can be something like this:
user ALL=(ALL) NOPASSWD: /usr/bin/wg, /usr/bin/wg-quick, /sbin/ip, /usr/bin/nmcli, /usr/libexec/nm-ifup, /usr/libexec/nm-ifdown, /sbin/dhclient
This gives the user specified the use of those commands, using sudo, but without entering a password and so should work within the scripts. All other commands, requiring elevated privileges, will necessitate inputting the password. The blanket entry needs to be present to allow use of SUDO for commands other than specified ...... user ALL=(ALL:ALL) NOPASSWD: ALL
I believe that the line above specifying the commands should be sufficient for running the posted scripts.
Further update!
After posting the original bash script (shown above), Cuig went on to recreate the script as a Python script. The Python script has the added benefit of dynamically showing, via the icon in the notification area, whether the Wireguard VPN connection is currently on or off.
You can find the Python script here in the PCLinuxOS forum. The Python script is a little bit larger (approximately 64 KiB, and the script is split between two posts due to its size). You can also download the Python version of the script from the magazine server, here. Just as with the bash scripts you download from the magazine website, store the file in the directory where you normally store your scripts, remove the “.txt” file extension, and make the file executable. To run it, the command should be “python3 wireguard-vpn-manager.py”.
|