Windscribe is a legitimate, privacy-focused VPN service with strong security features. It's regarded as one of the top VPN providers among enthusiasts in privacy-focused communities.
Moreover, you can see miles away from the download page that it takes Linux users seriously. From my personal experience with the client, this is, by far, the best Linux compatible VPN client in the market!
The client also works flawlessly inside a container, eliminating the need of layering the client on an immutable OS like Fedora Silverblue.
Here are reasons why you should consider Windscribe:
- There are many connection protocols available, WireGuard, Stealth, WStunnel, OpenVPN, IKEv2 (on mobile). The differences between them depend on your use case
- WireGuard is the fastest.
- Stealth is a censorship circumvention (China, Russia, Iran), restrictive networks.
- WStunnel is a last-resort option for the toughest firewalls or corporate networks.
- If that's not enough, there are more to circumvent censorship, decoy traffic, MAC spoofing, and GPS spoofing.
- Port forwarding is supported π€«
- Split tunneling is supported.
- CLI client for those on headless servers
- Many DNS resolver profiles, blocking malware, ads, and trackers by default.
- Static IP is available, along with static port for port forwarding. This is a killing feature for your remote home projects π§°
- Config files for OpenVPN, IKEv2 and WireGuard are available.
- Arcade sound for the connection! πΎπΉοΈ This feature sealed the deal for me π
- And many more, see all features!
Install Windscribe in a Container
ποΈ Table of contents:
- Install
distroboxπͺ©,podmanπ, andscreenπΊοΈ - Configure
distroboxto usepodman - Create a Container π¦οΈ
- Install Windscribe client in the Container
- Enable the Client's Helper
- Create a Launcher Script π and a Desktop File π₯οΈ on the Host
- Make the Container Update Itself Automatically, Zero Maintenance! π
π§§ And More:
- Config Your Firewall π₯ to Have Port Forwarding π Working Correctly
- For
ufwSystem - For
firewalldSystem
- For
- Check the Reach-ability of Your Opened Port ποΈ
- GUI way
- CLI way
π± Limitations
- App-based Split Tunneling πͺ
- Solution β οΈ
- A Container-safe π· Startup Delay
- Solution β οΈ
1. Install distrobox πͺ©, podman π, and screen πΊοΈ
The command will be differ based on your specific package manager. Refer to your distro's docs. For example, on Fedora Silverblue:
sudo rpm-ostree install distrobox podman screen
After the installation, reboot your system to activate the new layer. For other mutable distros, there's no need to reboot.
2. Configure distrobox to use podman
echo 'container_manager="podman"' > ~/.config/distrobox/distrobox.conf
3. Create a Container π¦οΈ
I use the official container image from Ubuntu, as I also use the image for ZeroTier and Cloudflare WARP. Otherwise, you could use openSUSE image instead:
registry.opensuse.org/opensuse/distrobox:latest
Because:
- It's easier to maintain as it uses a rolling release model, no need to worry about the EOL date of the image/OS.
- It offers some x86-64-v3 packages, free performance boost!, just by installing the
patterns-glibc-hwcaps-x86_64_v3package.
β οΈ Do NOT create a rootful
initcontainer, as it can cause ownership/permission conflicts on shared volumes between the host and other containers.
Creating a Container for Windscribe (Ubuntu Image)
distrobox create -i docker.io/library/ubuntu:latest -n vpn-dbx--root -H ~/distrobox/vpn-dbx--root --additional-packages "pipewire libxcb-shape0 libnl-genl-3-200" --volume /run/dbus/system_bus_socket:/run/dbus/system_bus_socket --additional-flags "--device=/dev/net/tun --cap-add=NET_ADMIN --cap-add=SYS_ADMIN" -r
- I add the
pipewirepackage to have the audio working for the arcade sound in the client πΎπΉοΈ -
libxcb-shape0andlibnl-genl-3-200are used by the client. -
/run/dbus/system_bus_socket,/dev/net/tun, along with--cap-add=NET_ADMIN--cap-add=SYS_ADMINare universally necessary for any app that wants to modify the state of your network. -
-ris used to create a rootful container, for obvious reason.
4. Install Windscribe client in the Container
Please refer to Windscribe's official download page.
Update All Packages in the Container
sudo apt update
Install the Official Client You Downloaded
For example:
sudo apt install ./windscribe_2.20.7_amd64.deb
5. Enable the Client's Helper
The client requires its helper running to function. Normally, if you install/layer the client directly on the system, the installer script will create a systemd unit for the helper automatically. But no worry, it can be done easily.
Create a Service Running the Helper
sudo nano /etc/systemd/system/windscribe-helper.service
Inside the file:
[Unit]
Description=Start Windscribe VPN Helper
After=network-online.target
Wants=network-online.target
RequiresMountsFor=%t/containers
StartLimitIntervalSec=30
StartLimitBurst=5
[Service]
Type=exec
ExecStartPre=/bin/podman start vpn-dbx--root
ExecStart=/bin/podman exec vpn-dbx--root bash -c "/opt/windscribe/helper"
Restart=on-failure
RestartSec=5
RemainAfterExit=yes
Create a Timer Triggering the Helper Service
sudo nano /etc/systemd/system/windscribe-helper.timer
Inside the file:
[Unit]
Description=A trigger to start Windscribe's helper on startup
[Timer]
OnBootSec=25
RandomizedDelaySec=10
[Install]
WantedBy=timers.target
Reload and Enable the Timer
sudo systemctl daemon-reload && sudo systemctl enable --now windscribe-helper.timer
The helper is now running in the background π
6. Create a Launcher Script π and a Desktop File π₯οΈ on the Host
It wouldn't be practical if you have to manually type a lengthy command in the terminal just to open a VPN client π
There are extra steps we will have to do to circumvent the security of our rootful container. But no worry, I will NOT do it in a way that compromises the security, as it's there for a reason.
I will simply use a wrapper script to launch the client. Then, put the script in a desktop file, so we can launch the app by clicking at a beautiful app icon like any other apps on your system.
The Wrapper Script
nano ~/.local/bin/windscribe-launcher.sh
Inside the file:
#!/bin/bash
# Define container name and flag file
CONTAINER_NAME="vpn-dbx--root"
READY_FLAG="/tmp/${CONTAINER_NAME}-ready.flag"
# Define the app's binary path and its StartupWMClass name
# The StartupWMClass has to be set up correctly. On GNOME, use Alt+F2 then lg to check the correct StartupWMClass of any app
APP_BINARY_PATH="/opt/windscribe/Windscribe"
APP_STARTUP_WM_CLASS_NAME="Windscribe"
: <<'SYSTEM_SERVICE_FILE_SECTION'
If you don't use/enable the system service file,
or you don't want to check whether it's running,
you can safely remove this whole section.
SYSTEM_SERVICE_FILE_SECTION
APP_HELPER_SERVICE_NAME="windscribe-helper.service"
# If invoked with "launch" argument, skip service start and proceed
if [ "$1" = "launch" ]; then
shift
else
# Check if the service is active (system service)
if ! systemctl is-active -q ${APP_HELPER_SERVICE_NAME}; then
echo "$APP_HELPER_SERVICE_NAME is not active. Attempting to start it..."
if [ "$(id -u)" -eq 0 ]; then
# Start directly
if systemctl start ${APP_HELPER_SERVICE_NAME}; then
echo "$APP_HELPER_SERVICE_NAME started successfully (as root)."
else
echo "Failed to start $APP_HELPER_SERVICE_NAME (as root)."
exit 1
fi
# Drop back to original user and re-run
exec runuser -u "${SUDO_USER:-$USER}" -- "$0" launch
else
# Use pkexec with retries
MAX_RETRIES=3
ATTEMPT=1
while [ $ATTEMPT -le $MAX_RETRIES ]; do
if pkexec systemctl start ${APP_HELPER_SERVICE_NAME}; then
echo "$APP_HELPER_SERVICE_NAME started successfully."
break
else
echo "Failed to start $APP_HELPER_SERVICE_NAME (attempt $ATTEMPT/$MAX_RETRIES)."
ATTEMPT=$((ATTEMPT + 1))
fi
done
if [ $ATTEMPT -gt $MAX_RETRIES ]; then
echo "Exceeded maximum retries. Exiting."
exit 1
fi
fi
# Re-execute to separate pkexec calls
exec "$0" launch
fi
fi
### End of SYSTEM_SERVICE_FILE_SECTION
# Check if the systemd scope is active (indicating the app is running)
if systemctl --user is-active -q ${APP_STARTUP_WM_CLASS_NAME}.scope; then
# Check if there's an existing app's window
exists=$(gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval "
global.get_window_actors()
.map(a => a.meta_window)
.some(w => w.get_wm_class() === '$APP_STARTUP_WM_CLASS_NAME');" | grep -o 'true')
if [ "$exists" = "true" ]; then
# Activate the existing window
gdbus call --session --dest org.gnome.Shell --object-path /org/gnome/Shell --method org.gnome.Shell.Eval "
const window = global.get_window_actors()
.map(a => a.meta_window)
.find(w => w.get_wm_class() === '$APP_STARTUP_WM_CLASS_NAME');
if (window) {
window.activate(global.get_current_time());
}"
else
# No window but scope active: Send resume command to persistent screen session (no prompt)
screen -S ${APP_STARTUP_WM_CLASS_NAME}-dbx -p 0 -X stuff "${APP_BINARY_PATH}\r"
fi
exit 0
fi
# If scope not active, ensure persistent screen session for container entry
if ! screen -ls | grep -q "${APP_STARTUP_WM_CLASS_NAME}-dbx"; then
# Clean up any old flag (optional, for safety)
rm -f "$READY_FLAG"
# Start detached screen session that enters the container and signals readiness (pkexec prompt for graphically auth)
# The command runs ONLY after the container is ready: touch the flag, then keep the session open with bash
screen -dmS ${APP_STARTUP_WM_CLASS_NAME}-dbx sh -c "DBX_SUDO_PROGRAM=pkexec distrobox-enter -r $CONTAINER_NAME -- bash -c 'touch $READY_FLAG && bash'"
# Wait for the flag file to appear (poll with a timeout for safety)
TIMEOUT=60 # Max seconds to wait
ELAPSED=0
while [ ! -f "$READY_FLAG" ]; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "Timeout: Container did not become ready in $TIMEOUT seconds."
# Optional: Kill the screen session if timed out
screen -S ${APP_STARTUP_WM_CLASS_NAME}-dbx -X quit
exit 1
fi
sleep 1
ELAPSED=$((ELAPSED + 1))
done
# Container is ready! Clean up the flag and proceed
rm -f "$READY_FLAG"
echo "Container is ready. Running next command..."
fi
# Send the initial launch command to the screen session
screen -S ${APP_STARTUP_WM_CLASS_NAME}-dbx -p 0 -X stuff "systemd-run --scope --user --unit=${APP_STARTUP_WM_CLASS_NAME}.scope $APP_BINARY_PATH &\r"
exit 0
Explanation π€
If you need it, this script works with any app in a rootful container. Here are all the variables you can change at the top of the script:
-
CONTAINER_NAME, as the name suggests. -
READY_FLAGis used to check the readiness of the container. -
APP_BINARY_PATHis the binary path of the app you want to use. In this case, it's/opt/windscribe/Windscribe. -
APP_STARTUP_WM_CLASS_NAMEis theStartupWMClassof the app. It has to be set up correctly. On GNOME, use Alt+F2 thenlgto check the correctStartupWMClassof any app. -
APP_HELPER_SERVICE_NAMEis the file name of the helper service we have created in the previous step. You don't have to wait for the helper to start, if you're in such a hurry, you can launch the app whenever you want, the script will help you launch the helper too if it's not already running!
Thanks to systemd-run and its scope unit, we can check easily whether the app's running, then use different commands to activate the app's window. I would say, not only this is native, but cleaner and more reliable than any other methods.
Then, I use gdbus to manipulate the state of the app's window. This is a native way on GNOME, so it works seamlessly on GNOME, but I don't know about the compatibility with other desktop environments.
But as I don't use other DEs, I wouldn't dare to publish the code that I didn't test or have any idea of. Therefore, if any of you have any idea on how to make it work with other DEs like KDE, COSMIC DE, or even tiling window managers like Hyprland, Niri, etc., please comment down below with a working script.
I also use screen to have a persistent session for distrobox-enter -r to our rootful container, so we don't have to enter our sudo password every single time we click on the app's icon π
The Desktop File
nano ~/.local/share/applications/windscribe.desktop
Inside the file:
[Desktop Entry]
Type=Application
Icon=/var/home/archerallstars/.local/share/icons/windscribe.png
Name=Windscribe
Comment=Start Windscribe VPN
Keywords=vpn;windscribe
Exec=/bin/bash /var/home/archerallstars/.local/bin/windscribe-launcher.sh
StartupWMClass=Windscribe
Terminal=false
β οΈ Replace
/var/home/archerallstarswith your host system's$HOMEabsolute path (the path without~).π‘ You can download the app icon easily from the Play Store ποΈ Then, replace the icon's path on the above with your icon's absolute path.
Now, you have the client 100% fully working!
You can check your public IP address and DNS resolver to see if they're matched with the ones showing in your Windscribe client, and also to see if there's any DNS leak by using https://dnscheck.tools/
For what it's worth, you might want to change your systemd-resolved's DNS over TLS setting to opportunistic, so it won't interfere with your VPN's DNS setup.
sudo nano /etc/systemd/resolved.conf
Then, in the file:
[Resolve]
DNSOverTLS=opportunistic
Lastly, restart systemd-resolved:
sudo systemctl restart systemd-resolved
7. Make the Container Update Itself Automatically, Zero Maintenance! π
Create a Service File
sudo nano /etc/systemd/system/vpn-dbx-upgrade.service
In the file:
[Unit]
Description=Upgrade vpn-dbx--root
After=network-online.target
Wants=network-online.target
RequiresMountsFor=%t/containers
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=exec
ExecStartPre=/bin/podman start vpn-dbx--root
ExecStart=/bin/podman exec vpn-dbx--root bash -c "apt update -y && apt full-upgrade -y"
Restart=on-failure
RestartSec=60
RemainAfterExit=yes
Create a Timer File
sudo nano /etc/systemd/system/vpn-dbx-upgrade.timer
In the file:
[Unit]
Description=Upgrade vpn-dbx--root daily.
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
Reload and Enable the Timer
sudo systemctl daemon-reload && sudo systemctl enable vpn-dbx-upgrade.timer
Config Your Firewall π₯ to Have Port Forwarding π Working Correctly
It depends on your host's firewall. For example, Ubuntu uses ufw, Fedora uses firewalld.
For ufw System
Check your firewall status:
sudo ufw status verbose
If it's enabled, you will need to open the correct port that you've opened in your Windscribe account's port forwarding page:
sudo ufw allow <port>/tcp && sudo ufw allow <port>/udp
β οΈ Change to your desired port number.
For firewalld System
1. Create a New Zone in firewalld
List all the available zones:
firewall-cmd --get-zones
We will create a new zone called vpn, if it's not presented yet, create a new one:
sudo firewall-cmd --permanent --new-zone=vpn
Reload firewalld for it to take effect:
sudo firewall-cmd --reload
Check all the available zones again:
firewall-cmd --get-zones
Now, vpn should be listed as one of the zones.
2. Finding the Interface's Name Using Network Manager
β οΈ It's possible to add a new interface to
firewalld's zones using the Network Manager, but it'll be conflicted with how Windscribe's client manages the network. Therefore, you should usefirewalldto manage its firewall's rules. Never use the Network Manager to manage your firewall rules!
firewalld, however, only knows and manages the interfaces that are bound to one of its zones. It cannot see any newly created interfaces that have never been introduced to it. Therefore, we use the Network Manager to list all the active interfaces on our system instead.
Finding your active connection name first:
nmcli connection show --active
It will return something like:
NAME UUID TYPE DEVICE
YourConnectionName xxxxxxxxxxxxxxxxxxxxxxxxxxxxx wifi xxxxxx
Note down your connection name. Usually, it will be something that has tun it its name. If you have connected to the VPN network, you can use an app like Resources to know the name for sure.
3. Adding the Interface to firewalld Permanently
sudo firewall-cmd --zone=vpn --change-interface='YourConnectionName' --permanent
Reload the firewall (to apply the change):
sudo firewall-cmd --reload
Also, check whether the interface is already in firewalld's zone (it should):
firewall-cmd --zone=vpn --list-interfaces
4. Adding the Required Ports to firewalld's Zone Permanently
List all the rules in vpn zone:
firewall-cmd --zone=vpn --list-all
π‘ If it doesn't show any port number after the
ports:entry, this meansfirewalldis blocking all incoming ports in this zone (vpn).
You can add your port like this:
sudo firewall-cmd --permanent --zone=vpn --add-port=<yourport>/tcp
sudo firewall-cmd --permanent --zone=vpn --add-port=<yourport>/udp
β οΈ Replace
<yourport>with the port you want to allow in the firewall.
Reload the firewall (to apply the change):
sudo firewall-cmd --reload
If you want to remove the port, since most of you would use an ephemeral port anyway:
sudo firewall-cmd --zone=public --remove-port=<yourport>/tcp --permanent
sudo firewall-cmd --zone=public --remove-port=<yourport>/udp --permanent
β οΈ Replace
<yourport>with the port you want to allow in the firewall.
π
firewalldis very complicate. If you can't connect with your local devices, you need to add mDNS and process forwarded traffic into this newvpnzone. It's the defaultpubliczone settings in Fedora that works for me with an app like LocalSend, for example.
sudo firewall-cmd --permanent --zone=vpn --add-service=mdns
sudo firewall-cmd --permanent --zone=vpn --add-forward
sudo firewall-cmd --reload
Check the Reach-ability of Your Opened Port ποΈ
First, please don't use any of the online port checkers like portchecker.co, for example. It never works for me...
The reliable way to test the reach-ability of your opened port is through torrent clients like Fragments, for example:
For Headless Folks
You can use this command to check the reach-ability of your opened port in the terminal like this:
p=<port_number>; curl -s https://portcheck.transmissionbt.com/$p | grep -q '^1' && echo -e "\033[1;32mβ
Port $p is OPEN\033[0m" || echo -e "\033[1;31mβ Port $p is CLOSED\033[0m"
β οΈ Please change the
<port_number>to the one you want to check.
This will return:
β
Port XXXXX is OPEN
Or:
β Port XXXXX is CLOSED
π± Limitations
App-based Split Tunneling πͺ
π‘ It should be noted that while we need to work around the app-based split tunneling, IP-based split tunneling is fully working. You can check your current IP routing table with
ip route showcommand. The client's GUI simply routes your desired IPs using the routing table.
The issue with the app-based split tunneling is expected, as we install the client in a container, so there's a layer of separation, even for a rootful container. Therefore, the client doesn't see any of your apps on the host or in other containers.
But no worry! This is a soft limitation, not a hard one or a blocker.
Solution β οΈ
If you really need this functionality, you can install the client in a rootless container with distrobox-create command's --unshare-netns option.
Basically, all routing, interfaces, and VPN tunnels stay confined to the container's network, so the VPN connection stay inside the container and won't be routed on the host, as the container simply lacks the capability/permission to do so.
You can create a container for the apps that you want to connect through the VPN, while leaving your host's connection outside the VPN tunnel. This behavior is exactly the same with Windscribe client's split tunneling in the inclusive mode:
β οΈ Please enable your Windscribe's VPN connection before creating a container. Because
pasta, the newpodman(v5.3+) container's network interface, even though it's a lot faster than before, it will only capture your host's current DNS addresses into the container.π‘ You can also specify multiple DNS addresses using
distrobox-create's--additional-flags "--dns 1.1.1.1 --dns 9.9.9.9", for example. If you use this flag, remember to also include at least one public DNS resolver, so you always have the connection.βοΈ Without the
--dnsflag,pastawill create a DNS forwarding entry, so whatever your host DNS is, it will be used in the container dynamically.β οΈ The VPN client can only use the DNS as per
pasta's snapshot inside the container in/etc/resolv.conf(changing this later on won't have any effects). Therefore, if the VPN's DNS address wasn't there from the start, it would regress back to your current DNS resolver on the host (throughpasta's DNS forwarding entry), hence leaking. In that case, please re-create the container again when you have the VPN connection up on the host πβοΈ Otherwise, you can create an
initcontainer with its ownsystemd-resolvedand all, but it won't integrate well with the host. Therefore, I don't recommend this route.
Now, finally, we can create a VPN only container:
distrobox-create -i registry.opensuse.org/opensuse/distrobox:latest -n vpn-apps-dbx -H ~/distrobox/vpn-apps-dbx --unshare-netns --volume /run/dbus/system_bus_socket:/run/dbus/system_bus_socket --additional-packages "libnl3-200 libxkbcommon-x11-0 libxcb-icccm4 libxcb-keysyms1 NetworkManager" --additional-flags "-p 127.0.0.1::57383"
Now, I use the openSUSE image instead of Ubuntu image, as a rolling release container is much easier to maintain, or rather, to not having to maintain anything π
--unshare-netnsso we use the container's network interface instead of the host's network interface (--network=host).-p 127.0.0.1::57383is basically tellpodmanthat only your host can talk to the container directly. Otherwise, others on the internet can bypass your host's firewall and talk to your container directly, hence a significant security risk. This is something you'll have to be careful when using the container's network interface (pasta) instead of--network=host. You can use any port number with-p 127.0.0.1::<container-port>.
π€© With a rootless container, you would be able to upgrade all your containers at once by using the
distrobox-upgrade --allcommand. Make it run daily in the background using a--usersystemdservice, like I wrote here, for example. Moreover, You can use Pods to manage your rootless containers beautifully:
The rest of the setup is the same as with a rootful container, except now, you don't have to circumvent the security of a rootful container. Meaning, everything works OOTB without the wrapper script.
β οΈ Remember that
/opt/windscribe/helperhave to be run withsudo, so when you export it withdistrobox-exportdon't forget to add--sudoflag. And now that this is a rootless container, remember to use asystemd's--userservice, not the root one (without the--userflag), to launch it on the container's start up.
After you set everything up and running, provided you set up your firewall correctly, you can check your opened port forwarding by using Transmission in the container:
A Container-safe π· Startup Delay
This is unfortunately, a hard limitation. There's nothing we can do about it. I put some delay in all my containers' initial process. I use 25 seconds delay for the client's helper here.
Otherwise, the system's booting process could crash entirely due to early-boot dependency race with the container runtime, storage, networking, or cgroup/setup layers.
See more here.
Solution β οΈ
We can simply install all of the apps that we want to have the VPN connection at all time inside a rootless container, as shown on the above.
With this solution, there will be no leak, as you have orderly control when launching the apps inside the container using systemd's After= and Requires= directives.
Therefore, it doesn't matter if you can't have the VPN connection right away on startup.
Thanks for reading π€
Cover Photo by Thomas Richter on Unsplash
A Container Photo by Sophie Cardinale on Unsplash
A WiFi Device Photo by Amal S on Unsplash
A Hand Photo by Frankie Mish on Unsplash









Top comments (0)