[GH-ISSUE #2931] Desktop client cannot recover or clean up an orphaned VPN session after GUI crash #1334

Open
opened 2026-10-13 20:40:26 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @romanio427 on GitHub (Aug 3, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2931

Environment

  • Windows 11, build 26100.8655
  • AmneziaVPN 5.0.0.5
  • Xray connection using tun2socks
  • Kill switch and site split tunneling enabled

Problem

If AmneziaVPN.exe exits unexpectedly while a VPN connection is active, the privileged VPN components continue running, but a newly started desktop client cannot discover or control them.

This leaves the machine with an orphaned VPN session: the GUI reports that it is disconnected while the service, tunnel process, virtual adapter, routes, DNS configuration, and kill-switch rules may still be active.

Reproduction

  1. Connect to a VPN configuration that uses Xray and tun2socks.
  2. Let the connection reach the Connected state.
  3. Terminate or crash AmneziaVPN.exe without using Disconnect.
  4. Confirm that AmneziaVPN-service.exe, tun2socks.exe, and the tun2socks Tunnel adapter remain active.
  5. Start AmneziaVPN.exe again.

Actual behavior

  • The restarted GUI displays no active VPN connection.
  • It does not adopt or expose the existing service-owned session.
  • Disconnect cannot stop the orphaned tunnel because the new client has no corresponding VpnProtocol object.
  • If the orphaned tunnel later loses upstream connectivity, networking remains affected by its routes or kill-switch state.
  • Recovery requires manually restarting/cleaning the service and tunnel components, or rebooting Windows.

Expected behavior

After the desktop GUI restarts, it should do one of the following:

  1. Query the service, detect the active session, restore its UI state, and allow the user to disconnect it; or
  2. Explicitly terminate and clean up an orphaned session, restoring routes, DNS, the TUN adapter, IPv6 routing, Xray, tun2socks, and kill-switch state.

An unexpected GUI exit should not leave the system in a state that the restarted application cannot manage.

Source-level findings

The current desktop lifecycle is split between the GUI and privileged service:

Consequently, the service owns the surviving resources, while the crashed GUI owned the only object that knew how to stop them.

Additional Windows cleanup finding: Wcmsvc

The Windows routing implementation reveals another cleanup resource that is not visible in the GUI state:

Therefore, if the GUI exits before it emits or handles that transition, the service can retain both its saved routes and a suspended Wcmsvc. A restarted GUI currently has no session object with which to trigger the matching cleanup. In the reported incident, the route table contained more than 9,000 tunnel-related entries, so the suspension threshold had been exceeded.

This is a source-supported cleanup consequence of the same ownership gap; it does not imply that Wcmsvc is suspended in every orphaned-session case.

Suggested direction

  • Make the privileged service the source of truth for active VPN session state.
  • Expose an IPC API to query, adopt, disconnect, or clean the current session.
  • Assign sessions to a client lease/heartbeat and perform service-side cleanup when ownership disappears, unless persistence is explicitly intended.
  • On desktop startup, reconcile actual service/tunnel state before presenting Disconnected.

The initial GUI crash used to reproduce this state is addressed separately in #2930. That PR removes a reentrant synchronous DNS wait, but it intentionally does not change service/session lifecycle behavior.

  • #2748 reports the same Windows crash signature (0xc00000fd in Qt6Core.dll) and a network interface remaining active with broken DNS after the GUI exits.
  • #1258 reports tun2socks and the tunnel surviving application/system termination, leaving networking broken after restart. It was closed as completed without a linked fix, but users subsequently reported that the problem remained relevant.
  • #2152 reports the desktop GUI showing Disconnected while the VPN is actually still connected after reboot.
  • #2791 reports a different Linux crash trigger, but the same cleanup failure: the TUN interface and routes remain while DNS/TCP traffic is broken. It is related lifecycle evidence, not the same crash root cause.

The current report remains separate because it reproduces the lifecycle failure on Windows with AmneziaVPN 5.0.0.5 and identifies the missing desktop recovery/service ownership path in the current source.

Originally created by @romanio427 on GitHub (Aug 3, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2931 ## Environment - Windows 11, build 26100.8655 - AmneziaVPN 5.0.0.5 - Xray connection using `tun2socks` - Kill switch and site split tunneling enabled ## Problem If `AmneziaVPN.exe` exits unexpectedly while a VPN connection is active, the privileged VPN components continue running, but a newly started desktop client cannot discover or control them. This leaves the machine with an orphaned VPN session: the GUI reports that it is disconnected while the service, tunnel process, virtual adapter, routes, DNS configuration, and kill-switch rules may still be active. ## Reproduction 1. Connect to a VPN configuration that uses Xray and `tun2socks`. 2. Let the connection reach the Connected state. 3. Terminate or crash `AmneziaVPN.exe` without using Disconnect. 4. Confirm that `AmneziaVPN-service.exe`, `tun2socks.exe`, and the `tun2socks Tunnel` adapter remain active. 5. Start `AmneziaVPN.exe` again. ## Actual behavior - The restarted GUI displays no active VPN connection. - It does not adopt or expose the existing service-owned session. - Disconnect cannot stop the orphaned tunnel because the new client has no corresponding `VpnProtocol` object. - If the orphaned tunnel later loses upstream connectivity, networking remains affected by its routes or kill-switch state. - Recovery requires manually restarting/cleaning the service and tunnel components, or rebooting Windows. ## Expected behavior After the desktop GUI restarts, it should do one of the following: 1. Query the service, detect the active session, restore its UI state, and allow the user to disconnect it; or 2. Explicitly terminate and clean up an orphaned session, restoring routes, DNS, the TUN adapter, IPv6 routing, Xray, `tun2socks`, and kill-switch state. An unexpected GUI exit should not leave the system in a state that the restarted application cannot manage. ## Source-level findings The current desktop lifecycle is split between the GUI and privileged service: - `XrayProtocol::startTun2Socks()` creates a privileged process replica and stores it only in the client-side `m_tun2socksProcess` member: https://github.com/amnezia-vpn/amnezia-client/blob/dev/client/core/protocols/xrayProtocol.cpp#L168-L178 - The service keeps the real `IpcServerProcess` in `IpcServer::m_processes`. There is no client ownership lease or removal on client disconnection: https://github.com/amnezia-vpn/amnezia-client/blob/dev/ipc/ipcserver.cpp#L31-L64 https://github.com/amnezia-vpn/amnezia-client/blob/dev/ipc/ipcserver.h#L48-L65 - Desktop `disconnectFromVpn()` returns after setting the UI state to Disconnected when `m_vpnProtocol` is null, without asking the service to find or clean an existing session: https://github.com/amnezia-vpn/amnezia-client/blob/dev/client/vpnConnection.cpp#L537-L548 - `restoreConnection()` is currently compiled only for Android: https://github.com/amnezia-vpn/amnezia-client/blob/dev/client/vpnConnection.h#L46-L48 https://github.com/amnezia-vpn/amnezia-client/blob/dev/client/vpnConnection.cpp#L485-L493 Consequently, the service owns the surviving resources, while the crashed GUI owned the only object that knew how to stop them. ## Additional Windows cleanup finding: `Wcmsvc` The Windows routing implementation reveals another cleanup resource that is not visible in the GUI state: - After adding more than 500 routes, `RouterWin::routeAddList()` suspends Windows Connection Manager (`Wcmsvc`): https://github.com/amnezia-vpn/amnezia-client/blob/dev/service/server/router_win.cpp#L145-L148 - `Wcmsvc` is resumed only from `RouterWin::clearSavedRoutes()` after the saved routes are deleted: https://github.com/amnezia-vpn/amnezia-client/blob/dev/service/server/router_win.cpp#L152-L202 - The implementation performs the suspension/resumption at process level through `NtSuspendProcess` / `NtResumeProcess`: https://github.com/amnezia-vpn/amnezia-client/blob/dev/service/server/router_win.cpp#L383-L400 https://github.com/amnezia-vpn/amnezia-client/blob/dev/service/server/router_win.cpp#L505-L513 - The desktop client requests `clearSavedRoutes()` only when it observes a transition to `Disconnected` or `Error`: https://github.com/amnezia-vpn/amnezia-client/blob/dev/client/vpnConnection.cpp#L162-L174 Therefore, if the GUI exits before it emits or handles that transition, the service can retain both its saved routes and a suspended `Wcmsvc`. A restarted GUI currently has no session object with which to trigger the matching cleanup. In the reported incident, the route table contained more than 9,000 tunnel-related entries, so the suspension threshold had been exceeded. This is a source-supported cleanup consequence of the same ownership gap; it does not imply that `Wcmsvc` is suspended in every orphaned-session case. ## Suggested direction - Make the privileged service the source of truth for active VPN session state. - Expose an IPC API to query, adopt, disconnect, or clean the current session. - Assign sessions to a client lease/heartbeat and perform service-side cleanup when ownership disappears, unless persistence is explicitly intended. - On desktop startup, reconcile actual service/tunnel state before presenting Disconnected. ## Related crash The initial GUI crash used to reproduce this state is addressed separately in #2930. That PR removes a reentrant synchronous DNS wait, but it intentionally does not change service/session lifecycle behavior. ## Existing related reports - #2748 reports the same Windows crash signature (`0xc00000fd` in `Qt6Core.dll`) and a network interface remaining active with broken DNS after the GUI exits. - #1258 reports `tun2socks` and the tunnel surviving application/system termination, leaving networking broken after restart. It was closed as completed without a linked fix, but users subsequently reported that the problem remained relevant. - #2152 reports the desktop GUI showing Disconnected while the VPN is actually still connected after reboot. - #2791 reports a different Linux crash trigger, but the same cleanup failure: the TUN interface and routes remain while DNS/TCP traffic is broken. It is related lifecycle evidence, not the same crash root cause. The current report remains separate because it reproduces the lifecycle failure on Windows with AmneziaVPN 5.0.0.5 and identifies the missing desktop recovery/service ownership path in the current source.
Author
Owner

@HeisLuka commented on GitHub (Sep 9, 2026):

I rechecked this against current dev, and the lifecycle/ownership gap described here still appears to be present.

There is one small clarification worth making to the original analysis.

Current VpnConnection::disconnectFromVpn() does call setConnectionState(Disconnected) when m_vpnProtocol is null. On desktop that means the generic Disconnected handler can still run things such as flushDns() / clearSavedRoutes() if that path is actually invoked.

However, that does not recover the orphaned XRay session.

The protocol-specific teardown is still coordinated by XrayProtocol::stop(). That path currently performs, among other things:

disableKillSwitch()
StartRoutingIpv6()
restoreResolvers()
deleteTun()
xrayStop()
kill/close m_tun2socksProcess

After a GUI crash, the restarted process no longer has the old m_vpnProtocol or its m_tun2socksProcess replica, so this teardown path is no longer reachable from the new GUI object graph.

The ownership split is still visible in current dev:

  • XrayProtocol::startTun2Socks() still obtains a client-side replica through IpcClient::CreatePrivilegedProcess();
  • the service still keeps the actual ProcessDescriptor objects in IpcServer::m_processes;
  • VpnConnection::restoreConnection() is still Android-only;
  • the service IPC interface still does not expose a VPN-session operation such as enumerate/query/adopt/abort;
  • IpcServer::cleanUp() only cleans logger state, so it is not a generic orphaned-VPN recovery operation.

So the underlying model is still:

privileged service owns long-lived resources
+
GUI object owns the lifecycle identity needed to tear them down
+
GUI dies
=
resources survive without an authoritative session owner

The partial clearSavedRoutes() behavior does not fix that model. It may clean one class of side effect, but it cannot establish which VPN generation owns the remaining TUN/XRay/tun2socks/kill-switch/DNS/IPv6 state, nor can it adopt or deterministically terminate that generation.

I think the durable fix is to make the privileged service authoritative for a generation-scoped VpnSession, for example:

sessionId / generation
profile/protocol identity
state: PREPARING / ACTIVE / STOPPING / ORPHANED
owned privileged processes
TUN interfaces
routes
DNS changes
kill-switch state
other OS side effects

Desktop startup should then reconcile against that service state before presenting Disconnected:

query active service session
-> compatible session exists:
       adopt it and expose Disconnect
   OR
-> stale/incompatible/orphaned generation:
       rollback that entire generation
-> only then allow a new connection

A client lease/heartbeat could define what happens when the GUI disappears: either the session intentionally remains alive and becomes adoptable, or the service rolls it back after lease loss.

The cleanup operation should also be generation-aware and idempotent. Killing only tun2socks, or only clearing saved routes, is not sufficient because the session owns multiple independent system mutations.

Useful regression cases would be:

  1. Kill the GUI while XRay is active -> restarted GUI discovers the existing service session.
  2. Adopt -> Disconnect -> complete network baseline is restored.
  3. Cleanup policy after owner loss -> process, XRay, TUN, routes, DNS, kill switch and IPv6 side effects are all reverted.
  4. Crash with a large saved-route set -> any Wcmsvc suspension belonging to that generation is eventually paired with its resume.
  5. Crash during PREPARING -> partially created resources are reconciled.
  6. A new VPN generation cannot start until the previous generation has been adopted or cleaned.

So as of current dev, this still looks unresolved and independent of the original GUI crash trigger itself.

<!-- gh-comment-id:5607297242 --> @HeisLuka commented on GitHub (Sep 9, 2026): I rechecked this against current `dev`, and the lifecycle/ownership gap described here still appears to be present. There is one small clarification worth making to the original analysis. Current `VpnConnection::disconnectFromVpn()` does call `setConnectionState(Disconnected)` when `m_vpnProtocol` is null. On desktop that means the generic `Disconnected` handler can still run things such as `flushDns()` / `clearSavedRoutes()` **if that path is actually invoked**. However, that does not recover the orphaned XRay session. The protocol-specific teardown is still coordinated by `XrayProtocol::stop()`. That path currently performs, among other things: ```text disableKillSwitch() StartRoutingIpv6() restoreResolvers() deleteTun() xrayStop() kill/close m_tun2socksProcess ``` After a GUI crash, the restarted process no longer has the old `m_vpnProtocol` or its `m_tun2socksProcess` replica, so this teardown path is no longer reachable from the new GUI object graph. The ownership split is still visible in current `dev`: * `XrayProtocol::startTun2Socks()` still obtains a client-side replica through `IpcClient::CreatePrivilegedProcess()`; * the service still keeps the actual `ProcessDescriptor` objects in `IpcServer::m_processes`; * `VpnConnection::restoreConnection()` is still Android-only; * the service IPC interface still does not expose a VPN-session operation such as enumerate/query/adopt/abort; * `IpcServer::cleanUp()` only cleans logger state, so it is not a generic orphaned-VPN recovery operation. So the underlying model is still: ```text privileged service owns long-lived resources + GUI object owns the lifecycle identity needed to tear them down + GUI dies = resources survive without an authoritative session owner ``` The partial `clearSavedRoutes()` behavior does not fix that model. It may clean one class of side effect, but it cannot establish which VPN generation owns the remaining TUN/XRay/tun2socks/kill-switch/DNS/IPv6 state, nor can it adopt or deterministically terminate that generation. I think the durable fix is to make the privileged service authoritative for a generation-scoped `VpnSession`, for example: ```text sessionId / generation profile/protocol identity state: PREPARING / ACTIVE / STOPPING / ORPHANED owned privileged processes TUN interfaces routes DNS changes kill-switch state other OS side effects ``` Desktop startup should then reconcile against that service state **before** presenting `Disconnected`: ```text query active service session -> compatible session exists: adopt it and expose Disconnect OR -> stale/incompatible/orphaned generation: rollback that entire generation -> only then allow a new connection ``` A client lease/heartbeat could define what happens when the GUI disappears: either the session intentionally remains alive and becomes adoptable, or the service rolls it back after lease loss. The cleanup operation should also be generation-aware and idempotent. Killing only `tun2socks`, or only clearing saved routes, is not sufficient because the session owns multiple independent system mutations. Useful regression cases would be: 1. Kill the GUI while XRay is active -> restarted GUI discovers the existing service session. 2. Adopt -> Disconnect -> complete network baseline is restored. 3. Cleanup policy after owner loss -> process, XRay, TUN, routes, DNS, kill switch and IPv6 side effects are all reverted. 4. Crash with a large saved-route set -> any `Wcmsvc` suspension belonging to that generation is eventually paired with its resume. 5. Crash during `PREPARING` -> partially created resources are reconciled. 6. A new VPN generation cannot start until the previous generation has been adopted or cleaned. So as of current `dev`, this still looks unresolved and independent of the original GUI crash trigger itself.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#1334
No description provided.