[PR #2835] fix: skip on-link routes during Windows route capture #2970

Open
opened 2026-10-14 21:06:34 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amnezia-client/pull/2835
Author: @eccodolf
Created: 7/15/2026
Status: 🔄 Open

Base: dev ← Head: fix/windows-route-capture-exclusions


📝 Commits (1)

  • 079a58b fix: skip on-link routes during Windows route capture

📊 Changes

7 files changed (+283 additions, -19 deletions)

View changed files

📝 CMakeLists.txt (+2 -0)
➕ client/platforms/windows/daemon/windowsroutecapturepolicy.h (+81 -0)
📝 client/platforms/windows/daemon/windowsroutemonitor.cpp (+12 -19)
📝 service/CMakeLists.txt (+4 -0)
📝 service/server/CMakeLists.txt (+1 -0)
➕ service/tests/CMakeLists.txt (+24 -0)
➕ service/tests/windows_route_capture_policy_tests.cpp (+159 -0)

📄 Description

Summary

Fix Windows route capture so AmneziaVPN does not clone directly connected local/on-link routes into the VPN interface during default-route capture.

When split tunneling/full-tunnel route capture is active, WindowsRouteMonitor::updateCapturedRoutes() currently clones non-default routes from the Windows route table to the VPN LUID with metric 0. Connected LAN and local virtual-network routes have no gateway, so cloning them sends local traffic through AmneziaVPN instead of the real Ethernet/Wi-Fi/Hyper-V/WSL/VirtualBox interface.

This change keeps route capture for routed prefixes, but skips routes whose NextHop is on-link/unspecified.

Fixes amnezia-vpn/amnezia-client#2830

Changes

  • Add WindowsRouteCapturePolicy, a small testable helper for route-capture decisions.
  • Skip routes from capture when they are:
    • already on the VPN interface;
    • default routes;
    • routes created by the route monitor itself;
    • explicitly excluded routes;
    • directly connected/on-link routes with no gateway (AF_UNSPEC, IPv4 0.0.0.0, or IPv6 ::).
  • Fix WindowsRouteMonitor::updateCapturedRoutes(int family) so it only uses the route table after GetIpForwardTable2() succeeds.
  • Register a new exclusion route before rerunning captured-route cleanup, so the new exclusion is visible to isRouteExcluded() during that pass.
  • Add Windows-only QtTest coverage for the route-capture policy.

Why this does not hard-code local networks

The fix intentionally does not special-case RFC1918 ranges such as 192.168.0.0/16, 10.0.0.0/8, or 172.16.0.0/12.

Local connected networks can be arbitrary: corporate routed ranges, CGNAT ranges, Hyper-V/WSL networks, VirtualBox host-only networks, public LAN prefixes, and other local adapters. Windows already marks connected routes by their missing/unspecified next hop, so the capture decision should use route semantics instead of address ranges.

Edge cases covered by tests

windows_route_capture_policy_tests covers:

  • routed IPv4 prefix is still captured;
  • routed IPv6 prefix is still captured;
  • VPN-interface routes are skipped;
  • default routes are skipped;
  • monitor-created routes are skipped;
  • same metric with a different protocol is not accidentally skipped;
  • explicitly excluded routes are skipped;
  • IPv4 on-link routes (0.0.0.0 next hop) are skipped;
  • IPv6 on-link routes (:: next hop) are skipped;
  • AF_UNSPEC next-hop routes are skipped.
  • amnezia-vpn/amnezia-client#2516: overlaps with exclusion batching and ordering. It partially helps large split lists and registers batch exclusions before one capture pass, but does not skip directly connected/on-link routes.
  • amnezia-vpn/amnezia-client#2555: changes WindowsRouteMonitor as part of locations/multi-tunnel work, but does not add an on-link route capture guard.
  • amnezia-vpn/amnezia-client#2609: changes Windows route monitoring for IPv6/dual-stack, but does not address local connected route capture.
  • amnezia-vpn/amnezia-client#2825: fixes Windows site split tunneling hostname resolution, but does not touch WindowsRouteMonitor::updateCapturedRoutes().

Verification

Passed:

git diff --check
cl /nologo /std:c++17 /c /TP client\platforms\windows\daemon\windowsroutecapturepolicy.h

Not run locally:

cmake -S . -B build-route-tests -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug
ctest --test-dir build-route-tests -R windows_route_capture_policy_tests

Reason: this machine does not have Conan available in PATH or Python modules, and project configure stops in cmake/recipes_bootstrap.cmake before test targets are generated:

Could not find CONAN_COMMAND using the following names: conan
python -m conan --version -> No module named conan
py -m conan --version -> No module named conan
conan --version -> command not found

Manual reproduction context

Observed bad routes on Windows when Amnezia split tunneling was enabled:

192.168.1.0/24   -> AmneziaVPN, metric 0
192.168.1.x/32   -> AmneziaVPN, metric 0
172.x.x.0/20     -> AmneziaVPN, metric 0

After deleting those cloned VPN routes, LAN routing returned to the physical/local interfaces. The fix prevents connected routes from being captured in the first place.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/amnezia-vpn/amnezia-client/pull/2835 **Author:** [@eccodolf](https://github.com/eccodolf) **Created:** 7/15/2026 **Status:** 🔄 Open **Base:** `dev` ← **Head:** `fix/windows-route-capture-exclusions` --- ### 📝 Commits (1) - [`079a58b`](https://github.com/amnezia-vpn/amnezia-client/commit/079a58b3b8e242e6dbf3596b95aa413c9d2cfe39) fix: skip on-link routes during Windows route capture ### 📊 Changes **7 files changed** (+283 additions, -19 deletions) <details> <summary>View changed files</summary> 📝 `CMakeLists.txt` (+2 -0) ➕ `client/platforms/windows/daemon/windowsroutecapturepolicy.h` (+81 -0) 📝 `client/platforms/windows/daemon/windowsroutemonitor.cpp` (+12 -19) 📝 `service/CMakeLists.txt` (+4 -0) 📝 `service/server/CMakeLists.txt` (+1 -0) ➕ `service/tests/CMakeLists.txt` (+24 -0) ➕ `service/tests/windows_route_capture_policy_tests.cpp` (+159 -0) </details> ### 📄 Description ## Summary Fix Windows route capture so AmneziaVPN does not clone directly connected local/on-link routes into the VPN interface during default-route capture. When split tunneling/full-tunnel route capture is active, `WindowsRouteMonitor::updateCapturedRoutes()` currently clones non-default routes from the Windows route table to the VPN LUID with metric `0`. Connected LAN and local virtual-network routes have no gateway, so cloning them sends local traffic through `AmneziaVPN` instead of the real Ethernet/Wi-Fi/Hyper-V/WSL/VirtualBox interface. This change keeps route capture for routed prefixes, but skips routes whose `NextHop` is on-link/unspecified. Fixes amnezia-vpn/amnezia-client#2830 ## Changes - Add `WindowsRouteCapturePolicy`, a small testable helper for route-capture decisions. - Skip routes from capture when they are: - already on the VPN interface; - default routes; - routes created by the route monitor itself; - explicitly excluded routes; - directly connected/on-link routes with no gateway (`AF_UNSPEC`, IPv4 `0.0.0.0`, or IPv6 `::`). - Fix `WindowsRouteMonitor::updateCapturedRoutes(int family)` so it only uses the route table after `GetIpForwardTable2()` succeeds. - Register a new exclusion route before rerunning captured-route cleanup, so the new exclusion is visible to `isRouteExcluded()` during that pass. - Add Windows-only QtTest coverage for the route-capture policy. ## Why this does not hard-code local networks The fix intentionally does not special-case RFC1918 ranges such as `192.168.0.0/16`, `10.0.0.0/8`, or `172.16.0.0/12`. Local connected networks can be arbitrary: corporate routed ranges, CGNAT ranges, Hyper-V/WSL networks, VirtualBox host-only networks, public LAN prefixes, and other local adapters. Windows already marks connected routes by their missing/unspecified next hop, so the capture decision should use route semantics instead of address ranges. ## Edge cases covered by tests `windows_route_capture_policy_tests` covers: - routed IPv4 prefix is still captured; - routed IPv6 prefix is still captured; - VPN-interface routes are skipped; - default routes are skipped; - monitor-created routes are skipped; - same metric with a different protocol is not accidentally skipped; - explicitly excluded routes are skipped; - IPv4 on-link routes (`0.0.0.0` next hop) are skipped; - IPv6 on-link routes (`::` next hop) are skipped; - `AF_UNSPEC` next-hop routes are skipped. ## Related PRs reviewed - amnezia-vpn/amnezia-client#2516: overlaps with exclusion batching and ordering. It partially helps large split lists and registers batch exclusions before one capture pass, but does not skip directly connected/on-link routes. - amnezia-vpn/amnezia-client#2555: changes `WindowsRouteMonitor` as part of locations/multi-tunnel work, but does not add an on-link route capture guard. - amnezia-vpn/amnezia-client#2609: changes Windows route monitoring for IPv6/dual-stack, but does not address local connected route capture. - amnezia-vpn/amnezia-client#2825: fixes Windows site split tunneling hostname resolution, but does not touch `WindowsRouteMonitor::updateCapturedRoutes()`. ## Verification Passed: ```text git diff --check cl /nologo /std:c++17 /c /TP client\platforms\windows\daemon\windowsroutecapturepolicy.h ``` Not run locally: ```text cmake -S . -B build-route-tests -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug ctest --test-dir build-route-tests -R windows_route_capture_policy_tests ``` Reason: this machine does not have Conan available in PATH or Python modules, and project configure stops in `cmake/recipes_bootstrap.cmake` before test targets are generated: ```text Could not find CONAN_COMMAND using the following names: conan python -m conan --version -> No module named conan py -m conan --version -> No module named conan conan --version -> command not found ``` ## Manual reproduction context Observed bad routes on Windows when Amnezia split tunneling was enabled: ```text 192.168.1.0/24 -> AmneziaVPN, metric 0 192.168.1.x/32 -> AmneziaVPN, metric 0 172.x.x.0/20 -> AmneziaVPN, metric 0 ``` After deleting those cloned VPN routes, LAN routing returned to the physical/local interfaces. The fix prevents connected routes from being captured in the first place. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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#2970
No description provided.