The VPN That Only Worked the Second Time
Date: 2025-09-09 Duration: About an hour of breaking and fixing Issue: LibreWolf VPN launcher only worked on second attempt Root Cause: Timing — VPN wasn’t ready when browser launched
The Setup
I run LibreWolf through a network namespace with PIA VPN. All browser traffic goes through the VPN. None of my real IP leaks. It’s a privacy setup that works well — when it works.
The launcher script:
exec sudo "$HOME/Scripts/pia_ns.sh" start /usr/bin/env librewolf \
--no-remote --new-instance --profile "$PROFILE_DIR" "$@"
Creates a namespace, connects OpenVPN, launches LibreWolf inside it. Simple.
The Problem
First launch of the day: fails. No connection. Browser sits there unable to reach anything.
Close it. Launch again. Works perfectly.
Every. Single. Time.
The second launch always worked. The first never did. Something about the timing was off.
The Theory
The namespace script starts OpenVPN and immediately launches LibreWolf. But VPN connections take a moment to fully establish. If LibreWolf tries to make requests before the tunnel is ready, those requests fail.
By the second launch, the VPN is already connected and stable. The namespace is warm. Everything works.
The “Fix” That Broke Everything
Added a VPN check before launching:
if ! sudo ip netns exec pia curl -s --max-time 10 ifconfig.io >/dev/null 2>&1; then
echo "ERROR: VPN not connected in namespace."
exit 1
fi
sleep 2
exec sudo "$HOME/Scripts/pia_ns.sh" start /usr/bin/env librewolf ...
The idea: verify the VPN works before launching the browser. Add a delay for stability.
The result: LibreWolf stopped launching entirely.
ERROR: VPN not connected in namespace. LibreWolf would leak your real IP.
Every time. Even when the VPN was definitely connected.
What Went Wrong
The check happened before the namespace started. The namespace script does both: creates the namespace AND starts the VPN. My verification was running outside the namespace, trying to test a VPN that didn’t exist yet.
Order of operations:
- Run verification → fails (no namespace yet)
- Exit with error
- Never reach the actual launch command
The script was checking if the VPN was ready before even trying to start it.
The Even Worse Fix
Added a cleanup trap:
cleanup() {
sudo ~/Scripts/pia_ns.sh stop >/dev/null 2>&1 || true
}
trap cleanup EXIT
The idea: clean up stale processes when LibreWolf closes.
The result: the cleanup ran when the script exited, which killed the namespace the moment LibreWolf tried to start. The browser never even had a chance.
The Actual Fix
Reverted to the original simple script:
#!/usr/bin/env bash
set -euo pipefail
PROFILE_DIR="$HOME/.librewolf/PIAProfile-LW"
exec sudo "$HOME/Scripts/pia_ns.sh" start /usr/bin/env librewolf \
--no-remote --new-instance --profile "$PROFILE_DIR" "$@"
No verification. No cleanup. Let the namespace script handle everything.
For the first-launch timing issue, the fix belongs in the namespace script itself, not the launcher wrapper. The namespace script should verify the VPN is connected before returning control to the caller.
The Lesson
Don’t fix the wrong layer. The launcher script just launches. The namespace script manages the namespace. Putting namespace logic in the launcher created timing conflicts.
Cleanup traps are dangerous. An EXIT trap runs on every exit, including successful launches. If your script execs into another process, you don’t want cleanup running during that transition.
Test your assumptions. I assumed the VPN check would run after the namespace existed. It ran before. That assumption cost an hour.
Why the Second Launch Worked
On first launch, the namespace doesn’t exist. The script creates it, starts the VPN, and launches the browser. If the timing is tight, the browser starts before the VPN is fully ready.
On second launch, the namespace already exists. The VPN is already connected. Everything is warm and stable. The browser starts immediately with a working connection.
The fix isn’t to verify in the wrapper — it’s to ensure the namespace script doesn’t return until the VPN is actually ready. That’s a problem for another day.
For now, the original script works. First launch takes an extra few seconds to stabilize. Second launch is instant. Good enough.
Sometimes the best fix is undoing the fix.