user@argobox:~/journal/2025-09-09-the-vpn-that-only-worked-the-second-time
$ cat entry.md

The VPN That Only Worked the Second Time

○ NOT REVIEWED

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:

  1. Run verification → fails (no namespace yet)
  2. Exit with error
  3. 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.