Summary
WireGuard connections fail on Python 3.14 with Error: Connection failed. Try connecting to a different server or check your network settings. The root cause is that tcpcheck.is_any_port_reachable() always returns False when called from within the CLI's asyncio event loop on Python 3.14, even though the target ports are genuinely reachable.
Environment
- OS: EndeavourOS (Arch Linux)
- python-proton-vpn-api-core: 5.2.5-1
- proton-vpn-cli: 1.0.1-1
- Python: 3.14.6
- NetworkManager: 1.58.0
Steps to Reproduce
- Set protocol to WireGuard (
~/.config/Proton/VPN/settings.json → "protocol": "wireguard")
- Run
protonvpn --verbose connect
Observed Output
CONN.CONNECT:START | Protocol: wireguard
TCP check: ip=<server_ip> ports=[443, 7770, 8443]
TCP check result: False
VPN server NOT reachable.
CONN:STATE_CHANGED | Error
This happens for every server selected. The same ports are confirmed reachable via direct socket tests (/dev/tcp, socket.connect_ex).
Root Cause
is_any_port_reachable() in tcpcheck.py wraps loop.run_in_executor() socket calls via asyncio.create_task + asyncio.as_completed. On Python 3.14, this combination silently returns False for all ports when called from within the CLI's existing event loop, even though the sockets would connect successfully if called directly.
Verified:
is_port_reachable(ip, port) called synchronously → True
asyncio.run(is_any_port_reachable(ip, ports)) in a fresh event loop → True
- Same call from within the CLI's event loop during connect →
False
There is also a pre-existing logic bug in the original as_completed loop: it returns on the very first completed task regardless of its result, meaning a single False (port refused quickly) would short-circuit before other ports succeed.
Workaround
Setting server_reachable = True unconditionally in networkmanager.py allows WireGuard to connect successfully, confirming the tunnel itself is fully functional.
Suggested Fix
Replace the create_task + as_completed pattern with asyncio.gather:
results = await asyncio.gather(
*[_is_port_reachable(port) for port in ports],
return_exceptions=True
)
return any(r is True for r in results)
Note: this also fixes the logic bug. Further investigation into why run_in_executor socket results are wrong inside the CLI event loop on Python 3.14 may be warranted.
Summary
WireGuard connections fail on Python 3.14 with
Error: Connection failed. Try connecting to a different server or check your network settings.The root cause is thattcpcheck.is_any_port_reachable()always returnsFalsewhen called from within the CLI's asyncio event loop on Python 3.14, even though the target ports are genuinely reachable.Environment
Steps to Reproduce
~/.config/Proton/VPN/settings.json→"protocol": "wireguard")protonvpn --verbose connectObserved Output
This happens for every server selected. The same ports are confirmed reachable via direct socket tests (
/dev/tcp,socket.connect_ex).Root Cause
is_any_port_reachable()intcpcheck.pywrapsloop.run_in_executor()socket calls viaasyncio.create_task+asyncio.as_completed. On Python 3.14, this combination silently returnsFalsefor all ports when called from within the CLI's existing event loop, even though the sockets would connect successfully if called directly.Verified:
is_port_reachable(ip, port)called synchronously →Trueasyncio.run(is_any_port_reachable(ip, ports))in a fresh event loop →TrueFalseThere is also a pre-existing logic bug in the original
as_completedloop: itreturns on the very first completed task regardless of its result, meaning a singleFalse(port refused quickly) would short-circuit before other ports succeed.Workaround
Setting
server_reachable = Trueunconditionally innetworkmanager.pyallows WireGuard to connect successfully, confirming the tunnel itself is fully functional.Suggested Fix
Replace the
create_task+as_completedpattern withasyncio.gather:Note: this also fixes the logic bug. Further investigation into why
run_in_executorsocket results are wrong inside the CLI event loop on Python 3.14 may be warranted.