banner
Previous Page
PCLinuxOS Magazine
PCLinuxOS
Article List
Disclaimer
Next Page

A Custom Script To Manage Your Wireguard VPN Connection


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.


Wireguard-VPN

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.


Wireguard-VPN

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


Wireguard-VPN

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.


Wireguard-VPN

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.

  1. #!/bin/bash
  2. # ======================================================
  3. # VPN Manager Script v1.6 2026-04-13
  4. # Features: Switching VPN, Stale VPN cleanup, File detection of “.conf” only
  5. # OS: PCLinuxOS 2026
  6. # Dependencies: Sudo, wireguard-tools, Zenity, papirus-icon-theme
  7. # ======================================================
  8. set -x # For debug output in a terminal
  9. cd /etc/wireguard
  10. Icon=/usr/share/icons/Papirus/48x48/apps/proton-vpn-logo.svg
  11. # --- Helper: Detect Active VPN ---
  12. get_active_vpn() {
  13.     # Must use sudo to read kernel state. We need the 2nd field.
  14.     local iface=$(sudo wg show 2>/dev/null | awk 'NR==1 {print $2}')
  15.     echo "$iface"
  16. }
  17. # --- Helper: Find Primary Network Interface ---
  18. get_primary_iface() {
  19.     # Try to find the interface with the default route
  20.     local iface=$(ip route | grep default | awk '{print $5}' | head -n1)
  21.     # Fallback: First non-loopback interface
  22.     if [[ -z "$iface" ]]; then
  23.        iface=$(ip link show 2>/dev/null | awk -F': ' '/^[0-9]+: (eth|wl|en)/ {print $2}' | head -n1)
  24.     fi
  25. # Last resort: eth0
  26.     if [[ -z "$iface" ]]; then
  27.         iface="eth0"
  28.     fi
  29.     echo "$iface"
  30. }
  31. # --- Helper: Cleanup ALL Stale Connections ---
  32. do_cleanup() {
  33.     echo ">>> Running Cleanup..."
  34. # Extract ONLY the interface names (first column of first line for each block)
  35. # Use 'awk' to find lines starting with "interface:" and print the 2nd field.
  36. local ifaces=$(sudo wg show 2>/dev/null | awk '/^interface:/ {print $2}')
  37. if [[ -z "$ifaces" ]]; then
  38.     zenity --window-icon=$Icon --info --text="Info No active connections found."
  39.     return
  40. fi
  41. local count=0
  42. for i in $ifaces; do
  43.     echo "Stopping: $i"
  44.     sudo wg-quick down "$i" 2>/dev/null || true
  45.     ((count++))
  46. done
  47. local PRIMARY=$(get_primary_iface)
  48. echo "Resetting network interface: $PRIMARY"
  49. if [ -f /usr/libexec/nm-ifdown ] && [ -f /usr/libexec/nm-ifup ]; then
  50.     sudo /usr/libexec/nm-ifdown "$PRIMARY"; sleep 1; sudo /usr/libexec/nm-ifup "$PRIMARY"
  51. else
  52.     sudo ip link set "$PRIMARY" down; sleep 1; sudo ip link set "$PRIMARY" up
  53. fi
  54.     zenity --window-icon=$Icon --info --text="Info Cleaned up $count connection(s)."
  55.     sleep 1
  56. }
  57. # --- Helper: Hard Kill ---
  58. do_hard_kill() {
  59.     local iface=$1
  60.     sudo wg-quick down "$iface"
  61.     local PRIMARY=$(get_primary_iface)
  62.     if [ -f /usr/libexec/nm-ifdown ] && [ -f /usr/libexec/nm-ifup ]; then
  63.         sudo /usr/libexec/nm-ifdown "$PRIMARY"; sleep 1; sudo /usr/libexec/nm-ifup "$PRIMARY"
  64.     else
  65.         sudo ip link set "$PRIMARY" down; sleep 1; sudo ip link set "$PRIMARY" up
  66.     fi
  67. }
  68. # ========================================================
  69. # MAIN LOOP
  70. # ========================================================
  71. while true; do
  72.     # 1. Check Status (Refreshed every loop)
  73.     CURRENT=$(get_active_vpn)
  74. if [[ -n "$CURRENT" ]]; then
  75.     TITLE=" Info SELECT A VPN TO ACTIVATE - Info ACTIVE: $CURRENT"
  76. else
  77.     TITLE=" Info SELECT A VPN TO ACTIVATE - Info DISCONNECTED"
  78. fi
  79.     # 2. Show Menu
  80.     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")
  81.     EXIT_CODE=$?
  82.     # 3. Cancel
  83.     if [ $EXIT_CODE -eq 1 ] && [ -z "$SELECTED" ]; then
  84.         exit 0
  85.     fi
  86.     # 4. Cleanup Button
  87.     if [ "$SELECTED" = "Cleanup" ]; then
  88.         do_cleanup
  89.         continue
  90.     fi
  91.     # 5. Deactivate Button
  92.     if [ "$SELECTED" = "Deactivate" ]; then
  93.         if [[ -n "$CURRENT" ]]; then
  94.        do_hard_kill "$CURRENT"
  95.        zenity --window-icon=$Icon --info --text="Info $CURRENT deactivated."
  96.         else
  97.        zenity --window-icon=$Icon --info --text="Info No VPN active."
  98.         fi
  99.         continue
  100. fi
  101.     # 6. Handle Activate Button - new or switch VPN
  102.     if [ $EXIT_CODE -eq 0 ]; then
  103.         if [ -z "$SELECTED" ]; then
  104.        zenity --window-icon=$Icon --warning --text="Info No file selected."
  105.        continue
  106.     fi
  107.     # If a VPN is active, stop it silently first
  108.     if [[ -n "$CURRENT" ]]; then
  109.         echo "Switching from $CURRENT to $SELECTED..."
  110.         sudo wg-quick down "$CURRENT"
  111.         # Wait for kernel to fully clear routes (Critical for stability)
  112.         sleep 2
  113.     fi
  114.     break
  115. fi
  116. done
  117. # --- Final Activation ---
  118. echo "🚀 Activating: $SELECTED"
  119. FILE="${SELECTED%.*}"
  120.     # Safety Check: Ensure no other VPN is active before starting
  121.     sleep 1
  122.     FINAL_CHECK=$(get_active_vpn)
  123.     if [[ -n "$FINAL_CHECK" ]]; then
  124.     # If the same interface is still there, try one last time to stop it
  125.     if [[ "$FINAL_CHECK" == "$CURRENT" ]]; then
  126.         echo "Warning: Interface $FINAL_CHECK still active. Retrying stop..."
  127.         sudo wg-quick down "$FINAL_CHECK"
  128.         sleep 1
  129.         FINAL_CHECK=$(get_active_vpn)
  130.     fi
  131.     if [[ -n "$FINAL_CHECK" ]]; then
  132.         zenity --window-icon=$Icon --error --text="Info Failed: Another VPN ($FINAL_CHECK) is still active."
  133.         exit 1
  134.     fi
  135. fi
  136. sudo wg-quick up "$FILE"
  137. if [ $? -eq 0 ]; then
  138.     zenity --window-icon=$Icon --info --text="Info VPN $FILE activated successfully."
  139. else
  140.     zenity --window-icon=$Icon --error --text="Info Failed to activate $FILE."
  141. 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”.



Previous Page              Top              Next Page