[GH-ISSUE #2830] Split tunneling adds local connected subnets to VPN interface routes and breaks LAN access on Windows #1289

Open
opened 2026-10-13 20:39:32 +00:00 by bsakaguchi · 3 comments
Owner

Originally created by @eccodolf on GitHub (Jul 14, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2830

Describe the bug
When split tunneling is enabled on Windows, AmneziaVPN adds local connected subnets to the VPN interface routes with metric 0.

This breaks LAN and local virtual network access because Windows routes local destinations through the VPN tunnel instead of their real local interfaces.

Example bad routes:

192.168.1.0/24 -> AmneziaVPN, metric 0
192.168.1.105/32 -> AmneziaVPN, metric 0
192.168.56.0/24 -> AmneziaVPN, metric 0
192.168.56.1/32 -> AmneziaVPN, metric 0
172.21.16.0/20 -> AmneziaVPN, metric 0
172.21.16.1/32 -> AmneziaVPN, metric 0

To Reproduce
Steps to reproduce the behavior:

  1. Go to AmneziaVPN on Windows.
  2. Enable split tunneling for apps/sites.
  3. Connect to an AmneziaWG / WireGuard VPN server.
  4. Check the IPv4 route table.
  5. See that local connected subnets are routed through AmneziaVPN.
  6. Try accessing a LAN host, for example 192.168.1.1.

Example command:

Test-NetConnection 192.168.1.1 -Port 80

Actual result:

InterfaceAlias : AmneziaVPN
SourceAddress : 10.8.1.6
TcpTestSucceeded : False

After manually deleting the bad routes, LAN access works again:

Remove-NetRoute -DestinationPrefix 192.168.1.0/24 -InterfaceAlias AmneziaVPN -Confirm:$false
Remove-NetRoute -DestinationPrefix 192.168.1.105/32 -InterfaceAlias AmneziaVPN -Confirm:$false
Remove-NetRoute -DestinationPrefix 192.168.56.0/24 -InterfaceAlias AmneziaVPN -Confirm:$false
Remove-NetRoute -DestinationPrefix 192.168.56.1/32 -InterfaceAlias AmneziaVPN -Confirm:$false
Remove-NetRoute -DestinationPrefix 172.21.16.0/20 -InterfaceAlias AmneziaVPN -Confirm:$false
Remove-NetRoute -DestinationPrefix 172.21.16.1/32 -InterfaceAlias AmneziaVPN -Confirm:$false

Then LAN routing works correctly:

InterfaceAlias : Wi-Fi
SourceAddress : 192.168.1.105
TcpTestSucceeded : True

Expected behavior
AmneziaVPN should not route already connected local subnets through the VPN interface when split tunneling is enabled.

Local connected routes should keep priority and continue using their real interfaces, for example:

192.168.1.0/24 -> Wi-Fi / Ethernet
192.168.56.0/24 -> VirtualBox Host-Only Network
172.21.16.0/20 -> Hyper-V vEthernet

Log files
Logs do not show route management errors. The tunnel itself appears healthy.

Observed in logs:

AmneziaVPN version 4.8.21.0
Sending handshake initiation
Received handshake response
Sending keepalive packet

Screenshots
Not applicable.

Desktop:

  • OS: Windows 11 25H2 x64
  • Version: AmneziaVPN 4.8.21.0

Server

  • OS: Linux VPS

Additional context
Current settings:

killSwitchEnabled = false
strictKillSwitchEnabled = false
appsSplitTunnelingEnabled = true
sitesSplitTunnelingEnabled = true

WFP state contains Amnezia/Mullvad split tunnel filters:

Amnezia-SplitTunnel-Sublayer
Mullvad Split Tunnel IPv4 Blocking Filter
Mullvad Split Tunnel IPv4 Permissive Filter

The tunnel daemon is started with:

Table = off

So these routes appear to be added by AmneziaVPN route/split-tunnel management, not directly by WireGuard.

When split tunneling is disabled and the bad routes are manually removed, LAN works correctly while VPN internet access continues to work.

Originally created by @eccodolf on GitHub (Jul 14, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2830 **Describe the bug** When split tunneling is enabled on Windows, AmneziaVPN adds local connected subnets to the VPN interface routes with metric 0. This breaks LAN and local virtual network access because Windows routes local destinations through the VPN tunnel instead of their real local interfaces. Example bad routes: 192.168.1.0/24 -> AmneziaVPN, metric 0 192.168.1.105/32 -> AmneziaVPN, metric 0 192.168.56.0/24 -> AmneziaVPN, metric 0 192.168.56.1/32 -> AmneziaVPN, metric 0 172.21.16.0/20 -> AmneziaVPN, metric 0 172.21.16.1/32 -> AmneziaVPN, metric 0 **To Reproduce** Steps to reproduce the behavior: 1. Go to AmneziaVPN on Windows. 2. Enable split tunneling for apps/sites. 3. Connect to an AmneziaWG / WireGuard VPN server. 4. Check the IPv4 route table. 5. See that local connected subnets are routed through AmneziaVPN. 6. Try accessing a LAN host, for example 192.168.1.1. Example command: Test-NetConnection 192.168.1.1 -Port 80 Actual result: InterfaceAlias : AmneziaVPN SourceAddress : 10.8.1.6 TcpTestSucceeded : False After manually deleting the bad routes, LAN access works again: Remove-NetRoute -DestinationPrefix 192.168.1.0/24 -InterfaceAlias AmneziaVPN -Confirm:$false Remove-NetRoute -DestinationPrefix 192.168.1.105/32 -InterfaceAlias AmneziaVPN -Confirm:$false Remove-NetRoute -DestinationPrefix 192.168.56.0/24 -InterfaceAlias AmneziaVPN -Confirm:$false Remove-NetRoute -DestinationPrefix 192.168.56.1/32 -InterfaceAlias AmneziaVPN -Confirm:$false Remove-NetRoute -DestinationPrefix 172.21.16.0/20 -InterfaceAlias AmneziaVPN -Confirm:$false Remove-NetRoute -DestinationPrefix 172.21.16.1/32 -InterfaceAlias AmneziaVPN -Confirm:$false Then LAN routing works correctly: InterfaceAlias : Wi-Fi SourceAddress : 192.168.1.105 TcpTestSucceeded : True **Expected behavior** AmneziaVPN should not route already connected local subnets through the VPN interface when split tunneling is enabled. Local connected routes should keep priority and continue using their real interfaces, for example: 192.168.1.0/24 -> Wi-Fi / Ethernet 192.168.56.0/24 -> VirtualBox Host-Only Network 172.21.16.0/20 -> Hyper-V vEthernet **Log files** Logs do not show route management errors. The tunnel itself appears healthy. Observed in logs: AmneziaVPN version 4.8.21.0 Sending handshake initiation Received handshake response Sending keepalive packet **Screenshots** Not applicable. **Desktop:** - OS: Windows 11 25H2 x64 - Version: AmneziaVPN 4.8.21.0 **Server** - OS: Linux VPS **Additional context** Current settings: killSwitchEnabled = false strictKillSwitchEnabled = false appsSplitTunnelingEnabled = true sitesSplitTunnelingEnabled = true WFP state contains Amnezia/Mullvad split tunnel filters: Amnezia-SplitTunnel-Sublayer Mullvad Split Tunnel IPv4 Blocking Filter Mullvad Split Tunnel IPv4 Permissive Filter The tunnel daemon is started with: Table = off So these routes appear to be added by AmneziaVPN route/split-tunnel management, not directly by WireGuard. When split tunneling is disabled and the bad routes are manually removed, LAN works correctly while VPN internet access continues to work.
Author
Owner

@Samarit commented on GitHub (Sep 4, 2026):

Bump

<!-- gh-comment-id:5541275078 --> @Samarit commented on GitHub (Sep 4, 2026): Bump
Author
Owner

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

I rechecked this against current dev, and the route table shown in this report is directly explainable by the current Windows route-capture implementation.

The problem does not appear to come from WireGuard's automatic routing. WireguardUtilsWindows::addInterface() explicitly uses:

Table = off

and Amnezia installs the relevant routes itself through the Windows IP Helper API.

Current path

When WireguardUtilsWindows::updateRoutePrefix() receives a /0 prefix, it does:

m_routeMonitor->setDetaultRouteCapture(true);

The purpose of this mechanism is apparently to deal with the fact that existing more-specific Windows routes would otherwise bypass a VPN default route.

WindowsRouteMonitor::updateCapturedRoutes() then enumerates the existing Windows routing table.

It skips:

  • the VPN interface itself;
  • default routes;
  • routes created as Amnezia exclusion routes;
  • explicitly excluded destinations;
  • loopback / broadcast / link-local / multicast destinations.

But it does not distinguish ordinary routed prefixes from connected/on-link LAN routes.

So entries such as:

192.168.1.0/24   Wi-Fi
192.168.1.105/32 Wi-Fi
192.168.56.0/24  VirtualBox
192.168.56.1/32  VirtualBox
172.21.16.0/20   Hyper-V
172.21.16.1/32   Hyper-V

all satisfy the current capture condition.

For every such route, the code then creates another route with:

data->InterfaceLuid.Value = m_luid; // VPN interface
data->DestinationPrefix = row->DestinationPrefix;
data->Metric = 0;
data->Protocol = MIB_IPPROTO_NETMGMT;

and installs it with:

CreateIpForwardEntry2(data);

That produces exactly the route shape reported here:

existing connected LAN prefix
        ↓
default-route capture sees it
        ↓
same prefix cloned onto AmneziaVPN
with metric 0
        ↓
Windows can select the VPN copy
        ↓
LAN/VirtualBox/Hyper-V traffic enters tunnel

So manually deleting those AmneziaVPN routes fixes access for the same reason: it restores the original connected route as the only/best route for that prefix.

I think the important distinction is route ownership/type

A full-tunnel implementation does need to prevent arbitrary more-specific routes from unexpectedly escaping the tunnel.

But a route representing a directly connected local network is not equivalent to an arbitrary route that should be captured.

The route policy should distinguish at least:

VPN-owned route
explicit VPN exclusion
directly connected/on-link local route
host/interface-address route
ordinary routed prefix
default route

rather than treating every non-default, non-excluded route as captureable.

There is already an excludeLocalNetworks() abstraction on the platform layer. On Windows it:

adds exclusion routes
+
enables LAN bypass in the firewall

but the current default-route capture itself does not automatically protect the connected LAN prefixes before cloning the route table.

Suggested invariant

Before enabling default-route capture:

snapshot physical routing state
        ↓
identify locally connected/on-link networks
        ↓
mark them according to explicit LAN policy
        ↓
capture only routes which are actually
supposed to follow the VPN

If normal Amnezia behavior is that LAN remains accessible, connected local prefixes should never be cloned onto the VPN interface.

If LAN capture is intentionally supported as a privacy/strict mode, it should be an explicit policy rather than an incidental result of generic route cloning.

For the configuration from this report, with:

killSwitchEnabled = false
strictKillSwitchEnabled = false

I would expect local connected routes to remain owned by their physical/virtual interfaces.

There is also a separate bug in the reconciliation path

Current WindowsRouteMonitor::updateCapturedRoutes(int family) contains:

PMIB_IPFORWARD_TABLE2 table;
DWORD error = GetIpForwardTable2(family, &table);

if (error != NO_ERROR) {
    updateCapturedRoutes(family, table);
    FreeMibTable(table);
}

This condition appears inverted.

The table should only be consumed when:

error == NO_ERROR

As currently written, a successful GetIpForwardTable2() does not trigger captured-route reconciliation, while an error path attempts to use/free table.

I don't think this inverted condition is required to explain the initial LAN-route cloning reported here, so I would treat it as an adjacent bug rather than the primary root cause.

But it can make the captured-route state stale after physical route changes and is worth fixing in the same area.

Regression cases

  1. Wi-Fi has 192.168.1.0/24 connected locally, VPN has /0:

    • 192.168.1.0/24 remains on Wi-Fi;
    • no duplicate metric-0 VPN route is created.
  2. VirtualBox/Hyper-V connected subnet:

    • remains reachable through its original interface.
  3. Interface host route such as 192.168.1.105/32:

    • is not cloned to the VPN interface.
  4. Ordinary more-specific Internet route which policy says must not bypass full tunnel:

    • can still be captured.
  5. Explicit site exclusion:

    • remains routed through the intended physical next hop.
  6. Physical route appears/disappears while VPN is active:

    • routeChanged() reconciliation updates state after successful GetIpForwardTable2().
  7. Disconnect:

    • all Amnezia-created cloned routes disappear;
    • pre-existing physical routes remain untouched.

So the current source strongly supports this report.

The core bug is not simply a bad Windows metric: default-route capture currently clones locally connected routes into the VPN because route classification is too broad.

<!-- gh-comment-id:5608061821 --> @HeisLuka commented on GitHub (Sep 9, 2026): I rechecked this against current `dev`, and the route table shown in this report is directly explainable by the current Windows route-capture implementation. The problem does not appear to come from WireGuard's automatic routing. `WireguardUtilsWindows::addInterface()` explicitly uses: ```text Table = off ``` and Amnezia installs the relevant routes itself through the Windows IP Helper API. ### Current path When `WireguardUtilsWindows::updateRoutePrefix()` receives a `/0` prefix, it does: ```cpp m_routeMonitor->setDetaultRouteCapture(true); ``` The purpose of this mechanism is apparently to deal with the fact that existing more-specific Windows routes would otherwise bypass a VPN default route. `WindowsRouteMonitor::updateCapturedRoutes()` then enumerates the existing Windows routing table. It skips: * the VPN interface itself; * default routes; * routes created as Amnezia exclusion routes; * explicitly excluded destinations; * loopback / broadcast / link-local / multicast destinations. But it does **not** distinguish ordinary routed prefixes from connected/on-link LAN routes. So entries such as: ```text 192.168.1.0/24 Wi-Fi 192.168.1.105/32 Wi-Fi 192.168.56.0/24 VirtualBox 192.168.56.1/32 VirtualBox 172.21.16.0/20 Hyper-V 172.21.16.1/32 Hyper-V ``` all satisfy the current capture condition. For every such route, the code then creates another route with: ```cpp data->InterfaceLuid.Value = m_luid; // VPN interface data->DestinationPrefix = row->DestinationPrefix; data->Metric = 0; data->Protocol = MIB_IPPROTO_NETMGMT; ``` and installs it with: ```cpp CreateIpForwardEntry2(data); ``` That produces exactly the route shape reported here: ```text existing connected LAN prefix ↓ default-route capture sees it ↓ same prefix cloned onto AmneziaVPN with metric 0 ↓ Windows can select the VPN copy ↓ LAN/VirtualBox/Hyper-V traffic enters tunnel ``` So manually deleting those AmneziaVPN routes fixes access for the same reason: it restores the original connected route as the only/best route for that prefix. ### I think the important distinction is route ownership/type A full-tunnel implementation does need to prevent arbitrary more-specific routes from unexpectedly escaping the tunnel. But a route representing a directly connected local network is not equivalent to an arbitrary route that should be captured. The route policy should distinguish at least: ```text VPN-owned route explicit VPN exclusion directly connected/on-link local route host/interface-address route ordinary routed prefix default route ``` rather than treating every non-default, non-excluded route as captureable. There is already an `excludeLocalNetworks()` abstraction on the platform layer. On Windows it: ```text adds exclusion routes + enables LAN bypass in the firewall ``` but the current default-route capture itself does not automatically protect the connected LAN prefixes before cloning the route table. ### Suggested invariant Before enabling default-route capture: ```text snapshot physical routing state ↓ identify locally connected/on-link networks ↓ mark them according to explicit LAN policy ↓ capture only routes which are actually supposed to follow the VPN ``` If normal Amnezia behavior is that LAN remains accessible, connected local prefixes should never be cloned onto the VPN interface. If LAN capture is intentionally supported as a privacy/strict mode, it should be an explicit policy rather than an incidental result of generic route cloning. For the configuration from this report, with: ```text killSwitchEnabled = false strictKillSwitchEnabled = false ``` I would expect local connected routes to remain owned by their physical/virtual interfaces. ### There is also a separate bug in the reconciliation path Current `WindowsRouteMonitor::updateCapturedRoutes(int family)` contains: ```cpp PMIB_IPFORWARD_TABLE2 table; DWORD error = GetIpForwardTable2(family, &table); if (error != NO_ERROR) { updateCapturedRoutes(family, table); FreeMibTable(table); } ``` This condition appears inverted. The table should only be consumed when: ```cpp error == NO_ERROR ``` As currently written, a successful `GetIpForwardTable2()` does not trigger captured-route reconciliation, while an error path attempts to use/free `table`. I don't think this inverted condition is required to explain the initial LAN-route cloning reported here, so I would treat it as an adjacent bug rather than the primary root cause. But it can make the captured-route state stale after physical route changes and is worth fixing in the same area. ### Regression cases 1. Wi-Fi has `192.168.1.0/24` connected locally, VPN has `/0`: * `192.168.1.0/24` remains on Wi-Fi; * no duplicate metric-0 VPN route is created. 2. VirtualBox/Hyper-V connected subnet: * remains reachable through its original interface. 3. Interface host route such as `192.168.1.105/32`: * is not cloned to the VPN interface. 4. Ordinary more-specific Internet route which policy says must not bypass full tunnel: * can still be captured. 5. Explicit site exclusion: * remains routed through the intended physical next hop. 6. Physical route appears/disappears while VPN is active: * `routeChanged()` reconciliation updates state after successful `GetIpForwardTable2()`. 7. Disconnect: * all Amnezia-created cloned routes disappear; * pre-existing physical routes remain untouched. So the current source strongly supports this report. The core bug is not simply a bad Windows metric: **default-route capture currently clones locally connected routes into the VPN because route classification is too broad.**
Author
Owner

@sinner4864 commented on GitHub (Sep 11, 2026):

I can reproduce very similar behavior on Windows with AmneziaVPN 5.0.1.5 (7d4f3e0), using an imported AmneziaWG profile. The UI identifies the protocol as “AmneziaWG (version 2)”.

Relevant configuration:
Address = 10.66.66.8/32
AllowedIPs = 0.0.0.0/0

My PC is on 192.168.0.0/24, and I connect to a local RDP server at 192.168.0.210:3389.

Observed behavior:

  1. With VPN disconnected, RDP works and Test-NetConnection returns TcpTestSucceeded: True through Ethernet.
  2. With VPN connected, Windows selects AmneziaVPN for the local destination, and the connection fails.
  3. Adding mstsc.exe to the app-based VPN bypass list did not help. Disabling app-based split tunneling did not resolve the problem either.
  4. IP-based split tunneling is unavailable. The UI reports: “The default server does not support split tunneling.”
  5. I added a temporary on-link route for 192.168.0.210/32 through Ethernet. A competing route for the same /32 appeared through AmneziaVPN with metric 0, and Find-NetRoute selected it.
  6. After removing only that competing VPN route, the selected interface changed to Ethernet. However, the TCP connection still failed with KillSwitch enabled.
  7. With KillSwitch disabled AND the competing VPN route removed after connecting, both Test-NetConnection and an actual RDP login succeeded while the VPN remained connected.
  8. The competing VPN route reappeared after reconnecting the VPN.

This appears to match the route-capture issue described here, with an additional obstacle when KillSwitch is enabled. The IPv4-only imported profile may also explain the unavailable IP-based split-tunneling settings.

Is there a supported way to allow access to this specific LAN server while keeping KillSwitch enabled for other traffic? Would PR #2835 or #3030 address this combination of issues, or should the KillSwitch behavior be reported separately?

I can provide sanitized routing output and additional test results if needed.

<!-- gh-comment-id:5637737525 --> @sinner4864 commented on GitHub (Sep 11, 2026): I can reproduce very similar behavior on Windows with AmneziaVPN 5.0.1.5 (7d4f3e0), using an imported AmneziaWG profile. The UI identifies the protocol as “AmneziaWG (version 2)”. Relevant configuration: Address = 10.66.66.8/32 AllowedIPs = 0.0.0.0/0 My PC is on 192.168.0.0/24, and I connect to a local RDP server at 192.168.0.210:3389. Observed behavior: 1. With VPN disconnected, RDP works and Test-NetConnection returns TcpTestSucceeded: True through Ethernet. 2. With VPN connected, Windows selects AmneziaVPN for the local destination, and the connection fails. 3. Adding mstsc.exe to the app-based VPN bypass list did not help. Disabling app-based split tunneling did not resolve the problem either. 4. IP-based split tunneling is unavailable. The UI reports: “The default server does not support split tunneling.” 5. I added a temporary on-link route for 192.168.0.210/32 through Ethernet. A competing route for the same /32 appeared through AmneziaVPN with metric 0, and Find-NetRoute selected it. 6. After removing only that competing VPN route, the selected interface changed to Ethernet. However, the TCP connection still failed with KillSwitch enabled. 7. With KillSwitch disabled AND the competing VPN route removed after connecting, both Test-NetConnection and an actual RDP login succeeded while the VPN remained connected. 8. The competing VPN route reappeared after reconnecting the VPN. This appears to match the route-capture issue described here, with an additional obstacle when KillSwitch is enabled. The IPv4-only imported profile may also explain the unavailable IP-based split-tunneling settings. Is there a supported way to allow access to this specific LAN server while keeping KillSwitch enabled for other traffic? Would PR #2835 or #3030 address this combination of issues, or should the KillSwitch behavior be reported separately? I can provide sanitized routing output and additional test results if needed.
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#1289
No description provided.