Thursday, March 19, 2026

ProtonVPN on Oracle Cloud ARM

 

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



No comments: