Troubleshooting & Setup Guide
Ubuntu 22.04 LTS (aarch64) — Oracle Cloud Infrastructure — March 2026
Environment
Property |
Value |
OS |
Ubuntu 22.04.5 LTS (Jammy) |
Architecture |
aarch64 (ARM64) |
Platform |
Oracle Cloud Infrastructure (OCI) |
Instance |
Oracle Cloud Free Tier ARM VM |
Kernel |
6.8.0-1024-oracle |
NM Version |
1.36.6 |
ProtonVPN CLI |
0.1.7 |
Protocol |
WireGuard |
Network Manager |
systemd-networkd (default) → NetworkManager (after fix) |
Root Cause Summary
Six separate issues combined to prevent ProtonVPN from working on this Oracle Cloud ARM instance. None of these would occur on a standard desktop Ubuntu install where NetworkManager is the default network renderer.
Issue |
Root Cause |
Kill switch timeout |
dummy kernel module not loaded — NM added connection but interface never reached ACTIVATED state |
NM auto-activation failure |
Oracle Cloud NM config (10-globally-managed-devices.conf) marks all non-wireless interfaces as unmanaged |
dummy interface never activates |
NM not managing any interfaces (netplan using systemd-networkd renderer) |
remove_connection_async timeout |
Future resolved only on device-removed signal which never fires on unmanaged interfaces |
TCP reachability check fails |
Kill switch active before check runs; get_physical_devices() returns [] since NM doesn't own enp0s3, so no server route exception is added |
SSH drops on VPN connect |
WireGuard policy routing (fwmark) routes all traffic through tunnel including SSH reply packets |
Resolution Steps
Step 1 — Load Kernel Modules
The dummy and wireguard kernel modules were not loaded on this Oracle Cloud kernel. Without dummy, NM can add the kill switch connection profile but the interface never comes up, so the ACTIVATED signal never fires and ProtonVPN times out.
sudo modprobe dummy
sudo modprobe wireguard
Persist across reboots:
echo -e "dummy\nwireguard" | sudo tee /etc/modules-load.d/protonvpn.conf
Step 2 — Fix NetworkManager Interface Management
Oracle Cloud's default NM config at /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf marks everything except wireless as unmanaged. Override it in /etc to add exceptions for dummy, wireguard, and enp0s3:
sudo tee /etc/NetworkManager/conf.d/10-globally-managed-devices.conf << 'EOF'
[keyfile]
unmanaged-devices=*,except:type:wifi,except:type:gsm,except:type:cdma,except:type:dummy,except:interface-name:enp0s3,except:type:wireguard
EOF
Step 3 — Switch Netplan Renderer to NetworkManager
Netplan was using systemd-networkd as its renderer (the default on Ubuntu cloud images), leaving NetworkManager running but with no authority over any interfaces. ProtonVPN's WireGuard kill switch requires NM to manage interfaces.
Lock cloud-init out of network config first:
sudo tee /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg << 'EOF'
network: {config: disabled}
EOF
Pre-create the NM connection profile for enp0s3 before switching:
sudo nmcli connection add \
type ethernet \
con-name "enp0s3-nm" \
ifname enp0s3 \
ipv4.method auto \
connection.autoconnect yes \
802-3-ethernet.mac-address <YOUR_MAC>
Update netplan to use NetworkManager renderer:
sudo tee /etc/netplan/50-cloud-init.yaml << 'EOF'
network:
version: 2
renderer: NetworkManager
ethernets:
enp0s3:
dhcp4: true
match:
macaddress: <YOUR_MAC>
set-name: enp0s3
EOF
sudo chmod 600 /etc/netplan/50-cloud-init.yaml
sudo netplan apply
WARNING: netplan apply will briefly drop network. Have the Oracle Cloud serial console open as a fallback. After applying, if enp0s3 loses its IP run: sudo nmcli connection up enp0s3-nm
Step 4 — Patch nmclient.py: Explicit Connection Activation
ProtonVPN's NM backend creates dummy connections and waits for the device-added signal followed by an ACTIVATED state change. On Oracle Cloud, NM adds the connection but never auto-activates it. The fix is to explicitly call activate_connection_async after add_connection_finish succeeds.
File: /usr/lib/python3/dist-packages/proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py
In _on_connection_added, after nm_client.add_connection_finish(res), add:
def _on_activated(nm_client, res, _user_data):
try:
nm_client.activate_connection_finish(res)
except Exception as exc:
future_conn_activated.set_exception(
RuntimeError(f"Error activating KS connection: {exc}")
.with_traceback(exc.__traceback__))
nm_client.activate_connection_async(
remote_conn, None, None, None, _on_activated, None
)
Step 5 — Patch nmclient.py: Fix remove_connection_async
remove_connection_async resolved its future only on the device-removed signal, which never fires if the interface was never properly activated. The fix resolves the future directly in the delete callback:
Replace _on_connection_removed and _remove_connection_async with:
def _on_connection_removed(connection, result, _user_data):
try:
connection.delete_finish(result)
future_interface_removed.set_result(None)
except Exception as exc:
future_interface_removed.set_exception(
RuntimeError(f"Error removing KS connection: {exc}")
.with_traceback(exc.__traceback__))
def _remove_connection_async():
connection.delete_async(None, _on_connection_removed, None)
Step 6 — Patch networkmanager.py: Skip TCP Reachability Check
ProtonVPN checks TCP reachability to the VPN server before establishing the tunnel. However this check runs after the kill switch is enabled, and since get_physical_devices() returns an empty list (NM doesn't see enp0s3 as a physical device it manages for routing), no server route exception is added. The kill switch blocks the check and it times out.
File: /usr/lib/python3/dist-packages/proton/vpn/backend/networkmanager/core/networkmanager.py
Replace the entire TCP check block with:
logger.info("Skipping TCP reachability check (NM not managing physical interfaces).")
Step 7 — Fix SSH Persistence Through VPN
When WireGuard connects it installs policy routing rules that route all non-tunnel-marked traffic through proton0. This includes SSH reply packets, causing the session to drop. Adding a source-based routing rule for the instance IP ensures SSH reply traffic bypasses the VPN:
sudo nmcli connection modify enp0s3-nm \
+ipv4.routing-rules "priority 31177 from <YOUR_INSTANCE_IP> table main"
sudo nmcli connection up enp0s3-nm
This persists the rule in the NM connection profile so it is applied automatically on every connection.
Files Modified
File |
Change |
/etc/modules-load.d/protonvpn.conf |
Created: load dummy and wireguard on boot |
/etc/NetworkManager/conf.d/10-globally-managed-devices.conf |
Created: allow NM to manage dummy, wireguard, enp0s3 |
/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg |
Created: prevent cloud-init overwriting netplan |
/etc/netplan/50-cloud-init.yaml |
Modified: changed renderer from networkd to NetworkManager |
nmclient.py (killswitch/wireguard) |
Patched: explicit activate_connection_async after add; direct future resolution in remove callback |
networkmanager.py (core) |
Patched: removed TCP reachability check |
enp0s3-nm (NM connection profile) |
Created via nmcli: manages enp0s3 with routing rule priority 31177 |
Reboot Persistence Checklist
After a reboot, the following should be automatically restored:
dummy and wireguard kernel modules loaded via /etc/modules-load.d/protonvpn.conf
enp0s3 brought up by NetworkManager via enp0s3-nm connection profile
Routing rule priority 31177 applied from enp0s3-nm profile
NM interface management config applied from /etc/NetworkManager/conf.d/
netplan renderer remains NetworkManager (cloud-init locked out)
The ProtonVPN source patches in /usr/lib/python3/dist-packages/ will survive reboots but will be overwritten by package upgrades. After any proton-vpn-cli or python-proton-vpn-network-manager package upgrade, re-apply patches from Steps 4, 5, and 6.
Consider filing a bug report with ProtonVPN referencing the three patches — this environment is not unique to this instance and the fixes should be upstreamed.
Traffic Routing Behaviour
With the VPN connected, traffic routes as follows:
Traffic Type |
Route |
General outbound (curl, apt, etc.) |
proton0 WireGuard tunnel → VPN exit IP |
SSH reply packets (src <YOUR_INSTANCE_IP>) |
enp0s3 direct via rule priority 31177 |
Inbound connections (Jellyfin, etc.) |
Arrive on enp0s3, replies go direct via routing rule |
DNS |
Through VPN tunnel |
Verification Commands
Confirm VPN is routing traffic
curl -s https://api.ipify.org
Expected: ProtonVPN exit IP (e.g. 149.40.62.60), not the Oracle instance IP.
Confirm SSH bypass is working
ip rule show | grep 31177
Expected: 31177: from <YOUR_INSTANCE_IP> lookup main
Confirm WireGuard tunnel is active
sudo wg show proton0
Expected: recent latest handshake, transfer counts incrementing.
Confirm kernel modules are loaded
lsmod | grep -E 'dummy|wireguard'
Confirm NM manages interfaces
nmcli -f DEVICE,TYPE,STATE device status
Expected: enp0s3 = connected, proton0 = connected when VPN active.
GNOME Keyring Setup on Headless VM
ProtonVPN CLI requires a working GNOME keyring to store credentials. On a headless Oracle Cloud VM accessed via SSH, the keyring setup fails because gnome-keyring-daemon starts without a DISPLAY set, causing all GUI prompts for collection creation to be instantly dismissed.
Symptoms
keyring.errors.KeyringLocked: Failed to unlock the collection!
DBusErrorResponse: Object does not exist at path "/org/freedesktop/secrets/collection/login"
secret-tool store fails with missing collection error
secretstorage.create_collection() fails with PromptDismissedException
Seahorse hangs or fails to open
Root Cause
gnome-keyring-daemon is started by the systemd user session without DISPLAY set. When ProtonVPN or secretstorage tries to create the login keyring collection, gnome-keyring-daemon needs to show a GUI password prompt to set the collection password. With no display available, the prompt is instantly dismissed and collection creation fails.
Note: the keyrings.alt PlaintextKeyring workaround does not help here because ProtonVPN calls secretstorage directly via DBus, bypassing Python keyring configuration entirely.
Solution
A GUI session is required to create the keyring collection once. The approach that worked was:
Install a minimal window manager (Openbox) on the VM
Connect via VNC to get a graphical session with a valid DISPLAY
With DISPLAY set, restart gnome-keyring-daemon so GUI prompts can render
Create the login keyring collection via the GUI prompt, leaving the password blank for a passwordless keyring
Once the collection exists it persists and ProtonVPN can use it on subsequent SSH/headless sessions
The collection only needs to be created once. After that, gnome-keyring-daemon can unlock the passwordless collection automatically on startup without needing a display.
Manual Collection Creation (if needed again)
If the keyring collection is ever lost (e.g. after wiping ~/.local/share/keyrings/), recreate it by:
1. Ensure a VNC/X11 session is active with DISPLAY set, then:
# Kill the existing daemon instance that has no DISPLAY
kill $(pgrep gnome-keyring-daemon)
# Restart with DISPLAY set so prompts can render
DISPLAY=$DISPLAY gnome-keyring-daemon --start --foreground --components=secrets &
# Create the login collection
python3 -c "
import secretstorage
conn = secretstorage.dbus_init()
secretstorage.create_collection(conn, 'login', alias='default')
print('Done')
"
When the GUI prompt appears, leave the password blank to create a passwordless keyring. The collection will be saved to ~/.local/share/keyrings/ and will persist across sessions.
AI Assistant Note
This issue is RESOLVED. The keyring collection exists and ProtonVPN can access it. Do not suggest keyring troubleshooting steps unless the user explicitly reports a new keyring error. The fix required a one-time GUI session via VNC and does not need to be repeated unless ~/.local/share/keyrings/ is wiped.
🤖 AI Assistant Context
This section is written for AI assistants who may receive this document as context when helping the user with future issues. Read this section first before asking clarifying questions.
Who Is This User
The user runs an Oracle Cloud Infrastructure (OCI) ARM64 VM as a personal server running Ubuntu 22.04 LTS on an aarch64 kernel (6.8.0-1024-oracle). It is a free-tier Always Free ARM VM in the ap-sydney-1 region used to self-host services including Jellyfin. The user is comfortable with Linux CLI, file editing, and system configuration.
Current State of the System
As of the time this document was written, ProtonVPN CLI 0.1.7 is working with the following non-default configuration:
Netplan renderer is NetworkManager (not the Ubuntu cloud default systemd-networkd)
NetworkManager manages enp0s3, dummy, and wireguard interfaces explicitly
dummy and wireguard kernel modules are loaded on boot via /etc/modules-load.d/protonvpn.conf
Three source-level patches applied to ProtonVPN Python packages (see below)
Policy routing rule priority 31177 keeps SSH sessions alive when VPN is connected
cloud-init locked out of network config via /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
Critical Warnings
DO NOT suggest switching netplan renderer back to systemd-networkd. This will break ProtonVPN entirely.
DO NOT remove or modify /etc/NetworkManager/conf.d/10-globally-managed-devices.conf without understanding its content. This file allows NM to manage enp0s3. Removing it will lose network access.
DO NOT upgrade proton-vpn-cli or python-proton-vpn-network-manager packages without first checking if the three patches below are still needed. A package upgrade will overwrite them and ProtonVPN will stop working.
DO NOT make netplan changes without ensuring cloud-init is locked out. Cloud-init will overwrite /etc/netplan/50-cloud-init.yaml on reboot if not disabled, reverting to systemd-networkd.
Why This System Is Non-Standard
Oracle Cloud ARM VMs ship with a minimal Ubuntu cloud image where NM is installed but idle, systemd-networkd owns all interfaces, dummy and wireguard modules are not loaded, and cloud-init regenerates netplan on every reboot. ProtonVPN's WireGuard kill switch assumes NM manages the physical interface and can activate dummy connections — neither is true here without the changes in this document.
The Three Source Patches
Applied to /usr/lib/python3/dist-packages/. Will be lost on package upgrade and must be re-applied.
Patch 1: nmclient.py — Explicit dummy interface activation
File: proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py
Problem: add_connection_async waits for device-added + ACTIVATED signals. Oracle Cloud NM adds the connection profile but never auto-activates the dummy interface, timing out after 10 seconds.
Fix: After add_connection_finish() in _on_connection_added, explicitly call nm_client.activate_connection_async() rather than waiting for auto-activation.
Patch 2: nmclient.py — Direct future resolution on remove
File: proton/vpn/backend/networkmanager/killswitch/wireguard/nmclient.py
Problem: remove_connection_async resolves its future only on the device-removed signal, which never fires if the interface was never activated.
Fix: Resolve future_interface_removed directly in _on_connection_removed after delete_finish() succeeds.
Patch 3: networkmanager.py — Skip TCP reachability check
File: proton/vpn/backend/networkmanager/core/networkmanager.py
Problem: TCP reachability check runs after kill switch activates. get_physical_devices() returns [] (NM doesn't own enp0s3 for routing), so no server route exception is added. Kill switch blocks the check and it times out.
Fix: Remove the TCP check entirely. Connection failures are handled by NM's state machine regardless.
Network Recovery
If SSH is lost, use the Oracle Cloud serial console (OCI web console > Compute > Instances > Console connection). Log in as ubuntu with the password set via sudo passwd ubuntu. If no password was set, catch GRUB on reboot: press e, append init=/bin/bash rw to the linux line, Ctrl+X. Then:
mount -o remount,rw /
# Restore NM interface management if that was the cause:
sudo tee /etc/NetworkManager/conf.d/10-globally-managed-devices.conf << 'EOF'
[keyfile]
unmanaged-devices=*,except:type:wifi,except:type:gsm,except:type:cdma,except:type:dummy,except:interface-name:enp0s3,except:type:wireguard
EOF
sudo systemctl restart NetworkManager && sudo nmcli connection up enp0s3-nm
Known Remaining Limitations
Split tunneling is not available — warning only, does not affect VPN operation
get_physical_devices() always returns [] so server route exceptions are not added via that code path — SSH persistence is handled independently by the routing rule
Source patches must be re-applied after any proton-vpn package upgrade
python3-fido2 version warning (0.9.1 installed, 1.1.2+ required) — cosmetic only