[GH-ISSUE #3037] Linux: AmneziaWG autoconnect fails after reboot due to missing exclusion route to VPN endpoint #1394

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

Originally created by @highlander-ekb on GitHub (Aug 25, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3037

Describe the bug

On Kubuntu 24.04 with AmneziaVPN 5.0.1.5 and AmneziaWG, automatic VPN connection after system boot may fail.

Amnezia creates the amn0 interface and installs the default split routes:

0.0.0.0/1 dev amn0 metric 1
128.0.0.0/1 dev amn0 metric 1

However, during the failed autoconnect attempt, the exclusion route to the VPN server endpoint is not created.

The log contains:

getGatewayAndIface: no gateway found
No default gateway available, skipping exclusion route

After that, Amnezia tries to establish the AmneziaWG connection, but the handshake cannot be completed and the client remains in the "Connecting" state indefinitely.

The log also contains:

Failed to send handshake initiation: network is unreachable

At this point there is no Internet connectivity.

If I manually click Disconnect and then Connect in the AmneziaVPN GUI, the connection succeeds immediately.

During the successful manual connection Amnezia correctly detects the physical gateway and creates the exclusion route:

Adding exclusion route for <VPN_SERVER_IP>/32
Gateway 192.168.1.1 for interface wlp0s20f3

After that the VPN and Internet connection work normally.

To Reproduce

  1. Configure AmneziaVPN to automatically connect using AmneziaWG.
  2. Reboot Kubuntu.
  3. Log in and wait for AmneziaVPN autoconnect.
  4. AmneziaVPN remains in the connecting state and Internet access is unavailable.
  5. Check the routing table. The following routes exist:

0.0.0.0/1 dev amn0 metric 1
128.0.0.0/1 dev amn0 metric 1

but there is no host route for the VPN endpoint via the physical Wi-Fi gateway.
6. In AmneziaVPN manually click Disconnect.
7. Click Connect.
8. The VPN connects successfully and Internet access is restored.
9. The routing table now contains an exclusion route similar to:

<VPN_SERVER_IP> via 192.168.1.1 dev wlp0s20f3

Expected behavior

AmneziaVPN should not install routes that direct the VPN server endpoint itself into amn0.

Before redirecting traffic through amn0, the client should ensure that an exclusion route to the VPN endpoint through the physical network gateway exists.

If the default gateway is not yet available during autoconnect, AmneziaVPN should wait/retry instead of continuing with a configuration in which the VPN endpoint becomes unreachable.

Additional troubleshooting

I tried adding the following systemd dependency to AmneziaVPN.service:

[Unit]
Wants=network-online.target
After=network-online.target

NetworkManager-wait-online.service is enabled and completes successfully.

This did NOT fix the issue.

Workaround

As a workaround, I added a permanent host route for the VPN endpoint to the NetworkManager Wi-Fi profile:

sudo nmcli connection modify ""
+ipv4.routes "<VPN_SERVER_IP>/32 192.168.1.1"

After adding this route, I rebooted the system again.

AmneziaVPN then connected automatically after boot and Internet access worked immediately without manually reconnecting the VPN.

This suggests that the problem is related to a race condition / missing exclusion route for the VPN endpoint during autoconnect.

Log files

A sanitized log showing the failed automatic connection followed by a successful manual Disconnect -> Connect can be provided/attached.

The real VPN server IP address has been replaced with <VPN_SERVER_IP>.

Screenshots

Not required to reproduce the issue. I can provide screenshots if needed.

Desktop (please complete the following information):

  • OS: Kubuntu 24.04 LTS
  • Desktop: KDE Plasma
  • Network management: NetworkManager
  • Connection: Wi-Fi
  • AmneziaVPN version: 5.0.1.5
  • Protocol: AmneziaWG
  • VPN type: Self-hosted
  • AmneziaVPN system service: enabled
  • NetworkManager-wait-online.service: enabled

amnezia-autoconnect-bug-sanitized.log

Originally created by @highlander-ekb on GitHub (Aug 25, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3037 **Describe the bug** On Kubuntu 24.04 with AmneziaVPN 5.0.1.5 and AmneziaWG, automatic VPN connection after system boot may fail. Amnezia creates the `amn0` interface and installs the default split routes: 0.0.0.0/1 dev amn0 metric 1 128.0.0.0/1 dev amn0 metric 1 However, during the failed autoconnect attempt, the exclusion route to the VPN server endpoint is not created. The log contains: getGatewayAndIface: no gateway found No default gateway available, skipping exclusion route After that, Amnezia tries to establish the AmneziaWG connection, but the handshake cannot be completed and the client remains in the "Connecting" state indefinitely. The log also contains: Failed to send handshake initiation: network is unreachable At this point there is no Internet connectivity. If I manually click Disconnect and then Connect in the AmneziaVPN GUI, the connection succeeds immediately. During the successful manual connection Amnezia correctly detects the physical gateway and creates the exclusion route: Adding exclusion route for <VPN_SERVER_IP>/32 Gateway 192.168.1.1 for interface wlp0s20f3 After that the VPN and Internet connection work normally. **To Reproduce** 1. Configure AmneziaVPN to automatically connect using AmneziaWG. 2. Reboot Kubuntu. 3. Log in and wait for AmneziaVPN autoconnect. 4. AmneziaVPN remains in the connecting state and Internet access is unavailable. 5. Check the routing table. The following routes exist: 0.0.0.0/1 dev amn0 metric 1 128.0.0.0/1 dev amn0 metric 1 but there is no host route for the VPN endpoint via the physical Wi-Fi gateway. 6. In AmneziaVPN manually click Disconnect. 7. Click Connect. 8. The VPN connects successfully and Internet access is restored. 9. The routing table now contains an exclusion route similar to: <VPN_SERVER_IP> via 192.168.1.1 dev wlp0s20f3 **Expected behavior** AmneziaVPN should not install routes that direct the VPN server endpoint itself into `amn0`. Before redirecting traffic through `amn0`, the client should ensure that an exclusion route to the VPN endpoint through the physical network gateway exists. If the default gateway is not yet available during autoconnect, AmneziaVPN should wait/retry instead of continuing with a configuration in which the VPN endpoint becomes unreachable. **Additional troubleshooting** I tried adding the following systemd dependency to `AmneziaVPN.service`: [Unit] Wants=network-online.target After=network-online.target `NetworkManager-wait-online.service` is enabled and completes successfully. This did NOT fix the issue. **Workaround** As a workaround, I added a permanent host route for the VPN endpoint to the NetworkManager Wi-Fi profile: sudo nmcli connection modify "<WiFi-profile>" \ +ipv4.routes "<VPN_SERVER_IP>/32 192.168.1.1" After adding this route, I rebooted the system again. AmneziaVPN then connected automatically after boot and Internet access worked immediately without manually reconnecting the VPN. This suggests that the problem is related to a race condition / missing exclusion route for the VPN endpoint during autoconnect. **Log files** A sanitized log showing the failed automatic connection followed by a successful manual Disconnect -> Connect can be provided/attached. The real VPN server IP address has been replaced with `<VPN_SERVER_IP>`. **Screenshots** Not required to reproduce the issue. I can provide screenshots if needed. **Desktop (please complete the following information):** - OS: Kubuntu 24.04 LTS - Desktop: KDE Plasma - Network management: NetworkManager - Connection: Wi-Fi - AmneziaVPN version: 5.0.1.5 - Protocol: AmneziaWG - VPN type: Self-hosted - AmneziaVPN system service: enabled - NetworkManager-wait-online.service: enabled [amnezia-autoconnect-bug-sanitized.log](https://github.com/user-attachments/files/31401369/amnezia-autoconnect-bug-sanitized.log)
Author
Owner

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

I rechecked this against current dev, and the failure described here is still directly explainable from the current activation control flow.

This does not look like only a network-online.target / systemd ordering problem. The more important issue is that failure to establish the endpoint escape route is currently treated as non-fatal, even though full-tunnel routing depends on it.

Current activation path

LocalSocketController::activate() explicitly puts the WireGuard/AWG server endpoint into excludedAddresses:

QJsonArray jsExcludedAddresses;
jsExcludedAddresses.append(
    wgConfig.value(amnezia::configKey::hostName));

so the intended invariant is clear: the VPN endpoint itself must remain reachable through the physical network.

Daemon::activate() then does:

create interface
-> assign interface IPs
-> bring interface up
-> add exclusion routes
-> configure peer
-> configure DNS
-> install allowed routes
-> Up

But the exclusion step currently is:

for (const QString& i : config.m_excludedAddresses) {
    addExclusionRoute(IPAddress(i));
}

The return value is ignored.

Daemon::addExclusionRoute() itself correctly propagates failure:

if (!wgutils()->addExclusionRoute(prefix)) {
    return false;
}

but activate() discards that result.

This matches the boot race exactly

On Linux, endpoint exclusion eventually reaches:

NetworkUtilities::getGatewayAndIface()

and current LinuxRouteMonitor::rtmSendRoute() explicitly does:

if (gateway.isEmpty()) {
    logger.warning()
        << "No default gateway available, skipping exclusion route";
    return false;
}

So during early boot the deterministic path is:

physical default gateway not ready
        ↓
endpoint exclusion requested
        ↓
LinuxRouteMonitor returns false
        ↓
Daemon::activate() ignores false
        ↓
activation continues

There is also a second endpoint-exclusion attempt in
WireguardUtilsLinux::updatePeer():

m_rtmonitor->addExclusionRoute(
    IPAddress(config.m_serverIpv4AddrIn));

but its result is also ignored.

updatePeer() returns only the result of the WireGuard UAPI command, so a successfully configured peer does not mean that the endpoint escape route exists.

Full-tunnel routing is then allowed to make the failure destructive

After updatePeer(), Daemon::activate() continues to
updateRoutePrefix().

For IPv4 /0, current Linux code intentionally installs:

0.0.0.0/1 -> amn0
128.0.0.0/1 -> amn0

instead of replacing the default route.

That is normally fine only if the VPN endpoint already has a more-specific physical route.

Without the exclusion route, the sequence becomes:

endpoint route missing
        ↓
0.0.0.0/1 + 128.0.0.0/1 installed
        ↓
VPN endpoint now resolves through amn0
        ↓
handshake packets depend on the tunnel
that they are supposed to establish
        ↓
connection cannot bootstrap

That explains why a later manual reconnect succeeds: by then NetworkManager has a usable gateway, the same exclusion call succeeds, and the endpoint remains outside amn0.

It also explains why the permanent NetworkManager /32 route is an effective workaround: it externally establishes the precondition that activation currently fails to enforce itself.

There is an additional route-commit robustness issue

LinuxRouteMonitor::rtmSendRoute() requests NLM_F_ACK, but its synchronous return value is currently only:

return (result == nlmsg->nlmsg_len);

where result is the result of sendto().

The actual NLMSG_ERROR / ACK is consumed later in nlsockReady(), and failures there are only logged.

So even when addExclusionRoute() returns true, that currently proves:

netlink request was sent

rather than necessarily:

kernel accepted and installed the route

For a bootstrap-critical endpoint route, I think the stronger condition is needed.

Suggested invariant

Before installing any route capable of capturing the VPN endpoint:

physical uplink is resolvable
AND
endpoint exclusion route is committed
AND
route-to-endpoint resolves outside amn0

Only after that should full-tunnel routes be installed.

Conceptually:

PREPARE
    resolve physical gateway/interface
    install endpoint escape route
    verify endpoint route

ACTIVATE
    configure peer
    DNS
    full-tunnel routes

COMMIT
    mark generation active

If the physical gateway is temporarily unavailable during boot, this should be a readiness state:

WAIT / RETRY

not:

continue with destructive routing

A bounded retry/backoff here would address the boot race without depending on whether network-online.target happens to correspond to the exact routing state Amnezia needs.

If any later activation step fails, the partially-created interface/routes/firewall/DNS state should also be rolled back.

Regression cases

  1. Start activation with no physical default gateway:

    • endpoint exclusion fails;
    • no 0.0.0.0/1 or 128.0.0.0/1 is installed;
    • endpoint is not captured by amn0;
    • activation waits/retries or fails cleanly.
  2. Default gateway appears later:

    • endpoint exclusion is installed first;
    • route readback confirms endpoint goes through the physical interface;
    • only then full-tunnel routes are installed.
  3. Netlink rejects the endpoint route:

    • activation must not continue based only on successful sendto().
  4. Before full-tunnel commit:

ip route get <VPN_ENDPOINT>

must resolve through a non-VPN interface.

  1. No manual Disconnect -> Connect should be required after normal boot.

So I think the report's root cause can be narrowed to:

a bootstrap-critical routing precondition can fail, but that failure is currently ignored and full-tunnel actuation continues anyway.

<!-- gh-comment-id:5607588556 --> @HeisLuka commented on GitHub (Sep 9, 2026): I rechecked this against current `dev`, and the failure described here is still directly explainable from the current activation control flow. This does not look like only a `network-online.target` / systemd ordering problem. The more important issue is that failure to establish the endpoint escape route is currently treated as non-fatal, even though full-tunnel routing depends on it. ### Current activation path `LocalSocketController::activate()` explicitly puts the WireGuard/AWG server endpoint into `excludedAddresses`: ```cpp QJsonArray jsExcludedAddresses; jsExcludedAddresses.append( wgConfig.value(amnezia::configKey::hostName)); ``` so the intended invariant is clear: the VPN endpoint itself must remain reachable through the physical network. `Daemon::activate()` then does: ```text create interface -> assign interface IPs -> bring interface up -> add exclusion routes -> configure peer -> configure DNS -> install allowed routes -> Up ``` But the exclusion step currently is: ```cpp for (const QString& i : config.m_excludedAddresses) { addExclusionRoute(IPAddress(i)); } ``` The return value is ignored. `Daemon::addExclusionRoute()` itself correctly propagates failure: ```cpp if (!wgutils()->addExclusionRoute(prefix)) { return false; } ``` but `activate()` discards that result. ### This matches the boot race exactly On Linux, endpoint exclusion eventually reaches: ```cpp NetworkUtilities::getGatewayAndIface() ``` and current `LinuxRouteMonitor::rtmSendRoute()` explicitly does: ```cpp if (gateway.isEmpty()) { logger.warning() << "No default gateway available, skipping exclusion route"; return false; } ``` So during early boot the deterministic path is: ```text physical default gateway not ready ↓ endpoint exclusion requested ↓ LinuxRouteMonitor returns false ↓ Daemon::activate() ignores false ↓ activation continues ``` There is also a second endpoint-exclusion attempt in `WireguardUtilsLinux::updatePeer()`: ```cpp m_rtmonitor->addExclusionRoute( IPAddress(config.m_serverIpv4AddrIn)); ``` but its result is also ignored. `updatePeer()` returns only the result of the WireGuard UAPI command, so a successfully configured peer does not mean that the endpoint escape route exists. ### Full-tunnel routing is then allowed to make the failure destructive After `updatePeer()`, `Daemon::activate()` continues to `updateRoutePrefix()`. For IPv4 `/0`, current Linux code intentionally installs: ```text 0.0.0.0/1 -> amn0 128.0.0.0/1 -> amn0 ``` instead of replacing the default route. That is normally fine **only if the VPN endpoint already has a more-specific physical route**. Without the exclusion route, the sequence becomes: ```text endpoint route missing ↓ 0.0.0.0/1 + 128.0.0.0/1 installed ↓ VPN endpoint now resolves through amn0 ↓ handshake packets depend on the tunnel that they are supposed to establish ↓ connection cannot bootstrap ``` That explains why a later manual reconnect succeeds: by then NetworkManager has a usable gateway, the same exclusion call succeeds, and the endpoint remains outside `amn0`. It also explains why the permanent NetworkManager `/32` route is an effective workaround: it externally establishes the precondition that activation currently fails to enforce itself. ### There is an additional route-commit robustness issue `LinuxRouteMonitor::rtmSendRoute()` requests `NLM_F_ACK`, but its synchronous return value is currently only: ```cpp return (result == nlmsg->nlmsg_len); ``` where `result` is the result of `sendto()`. The actual `NLMSG_ERROR` / ACK is consumed later in `nlsockReady()`, and failures there are only logged. So even when `addExclusionRoute()` returns `true`, that currently proves: ```text netlink request was sent ``` rather than necessarily: ```text kernel accepted and installed the route ``` For a bootstrap-critical endpoint route, I think the stronger condition is needed. ### Suggested invariant Before installing any route capable of capturing the VPN endpoint: ```text physical uplink is resolvable AND endpoint exclusion route is committed AND route-to-endpoint resolves outside amn0 ``` Only after that should full-tunnel routes be installed. Conceptually: ```text PREPARE resolve physical gateway/interface install endpoint escape route verify endpoint route ACTIVATE configure peer DNS full-tunnel routes COMMIT mark generation active ``` If the physical gateway is temporarily unavailable during boot, this should be a readiness state: ```text WAIT / RETRY ``` not: ```text continue with destructive routing ``` A bounded retry/backoff here would address the boot race without depending on whether `network-online.target` happens to correspond to the exact routing state Amnezia needs. If any later activation step fails, the partially-created interface/routes/firewall/DNS state should also be rolled back. ### Regression cases 1. Start activation with no physical default gateway: * endpoint exclusion fails; * no `0.0.0.0/1` or `128.0.0.0/1` is installed; * endpoint is not captured by `amn0`; * activation waits/retries or fails cleanly. 2. Default gateway appears later: * endpoint exclusion is installed first; * route readback confirms endpoint goes through the physical interface; * only then full-tunnel routes are installed. 3. Netlink rejects the endpoint route: * activation must not continue based only on successful `sendto()`. 4. Before full-tunnel commit: ```text ip route get <VPN_ENDPOINT> ``` must resolve through a non-VPN interface. 5. No manual Disconnect -> Connect should be required after normal boot. So I think the report's root cause can be narrowed to: **a bootstrap-critical routing precondition can fail, but that failure is currently ignored and full-tunnel actuation continues anyway.**
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#1394
No description provided.