[PR #3030] fix(windows): stabilize site split tunneling #3060

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

📋 Pull Request Information

Original PR: https://github.com/amnezia-vpn/amnezia-client/pull/3030
Author: @makkoar
Created: 8/24/2026
Status: 🔄 Open

Base: dev ← Head: fix/windows-site-split-tunneling


📝 Commits (3)

  • 83977b2 fix: refresh domain split tunneling routes
  • ab5c79b fix: apply Windows exclusion routes transactionally
  • 5dc9dd1 fix: use type-safe route address copies

📊 Changes

13 files changed (+956 additions, -196 deletions)

View changed files

📝 client/core/controllers/ipSplitTunnelingController.cpp (+129 -49)
📝 client/core/controllers/ipSplitTunnelingController.h (+11 -5)
📝 client/core/repositories/secureAppSettingsRepository.cpp (+44 -6)
📝 client/core/repositories/secureAppSettingsRepository.h (+5 -3)
📝 client/daemon/daemon.cpp (+66 -13)
📝 client/daemon/daemon.h (+3 -1)
📝 client/daemon/wireguardutils.h (+21 -1)
📝 client/platforms/windows/daemon/windowsroutemonitor.cpp (+241 -88)
📝 client/platforms/windows/daemon/windowsroutemonitor.h (+15 -2)
📝 client/platforms/windows/daemon/wireguardutilswindows.cpp (+161 -0)
📝 client/platforms/windows/daemon/wireguardutilswindows.h (+3 -0)
📝 client/vpnConnection.cpp (+228 -28)
📝 client/vpnConnection.h (+29 -0)

📄 Description

Summary

This PR makes domain-based split tunneling more reliable for WireGuard and AmneziaWG on Windows. It keeps all current IPv4 addresses for each hostname, refreshes them before connection and reconnection, applies exclusion routes as a batch, and makes Windows route reconciliation resilient to duplicate and failed route operations.

Problem

I originally encountered two user-visible failures:

  • an IPv4 CIDR exclusion such as x.x.x.0/24 could be accepted and remain visible in settings without receiving a working Windows bypass route;
  • domains and networks listed as exclusions could still be routed through the VPN tunnel, especially with IPv4-only WireGuard/AmneziaWG profiles.

Site exclusions could become incomplete or unstable for several independent reasons:

  • a hostname could resolve to multiple IPv4 addresses, while stored and imported entries did not consistently retain the complete set;
  • cached addresses could be stale when a WireGuard/AmneziaWG tunnel was started or reconnected;
  • DNS resolution performed after tunnel activation could use a different route or resolver than the system and hosts configuration expected;
  • IPv4-only WireGuard configurations were incorrectly treated as incompatible with site split tunneling unless both 0.0.0.0/0 and ::/0 were present;
  • Windows exclusion routes were installed one by one while route-change notifications triggered repeated full reconciliations, which made large exclusion lists slow and prone to transient failures;
  • route installation errors were not propagated consistently, so activation could continue with a partially applied exclusion list even though every entry remained visible in settings;
  • repeated protocol setup could register duplicate reconnect handlers;

These issues could result in slow page loads, intermittent connections, or local/private hostnames timing out while the VPN was connected, even though the same destinations worked without the VPN.

Changes

Domain entries and persistence

  • Normalize URLs and hostnames with QUrl, including IDN-to-ACE conversion and removal of a trailing dot.
  • Validate IPv4 CIDRs strictly and reject malformed domain names and imported addresses.
  • Store every unique IPv4 address associated with a hostname instead of a single address.
  • Merge duplicate entries during JSON import.
  • Export the full address list through ips while retaining the legacy ip field for backward compatibility.
  • Continue reading legacy settings where a site contains one string instead of a string list.
  • Associate asynchronous DNS results with the route mode in which the lookup was started, avoiding writes to a different list if the user changes the mode before the response arrives.

DNS refresh before tunnel activation

  • Resolve domain exclusions before starting WireGuard or AmneziaWG on desktop platforms.
  • Use the system resolver, including local hosts mappings.
  • Limit resolution to 16 concurrent lookups and a total preparation timeout of 10 seconds.
  • Replace stored addresses only for domains that resolved successfully.
  • Keep the last known addresses for unresolved domains so a temporary DNS failure does not erase a working configuration.
  • Repeat the refresh on reconnect and cancel obsolete lookups on a new connection attempt, disconnect, or destruction.

WireGuard and AmneziaWG configuration handling

  • Parse AllowedIPs consistently from JSON arrays, strings, and native configuration text, regardless of comma spacing.
  • Enable IPv4 site routes when 0.0.0.0/0 is present; ::/0 is no longer required for the currently IPv4-based site exclusion list.
  • Use unique queued IPC subscriptions so repeated protocol creation does not multiply reconnect callbacks.

Windows route handling

  • Install exclusion routes as a batch and reuse one routing-table snapshot per address family.
  • Apply reference-count updates only after the batch succeeds.
  • Roll back newly added routes when activation or server switching fails.
  • Propagate route errors instead of continuing with a partially configured exclusion list.
  • Select and refresh the best physical interface and gateway for every exclusion route.
  • Treat an already existing equivalent route as usable without claiming ownership of it.
  • Skip duplicate and non-routable destinations.
  • Debounce bursts of NotifyRouteChange2 events and safely handle notifications without a route row.
  • Avoid repeated attempts to clone the same conflicting captured route during one reconciliation pass.
  • Clean up only routes owned by the monitor.
  • For very large batches, temporarily pause wcmSvc route churn while applying the routes. This optimization is best-effort and falls back to normal batch installation if the required Windows APIs or privilege are unavailable.

Related issues:

  • #2248 — slow and unstable Windows connection setup with large split-tunneling lists.
  • #2907 — site split tunneling not applied to imported AmneziaWG profiles.
  • #2939 — CIDR prefix handling for imported site entries.
  • #3020 — quadratic Windows route setup for large CIDR exclusion lists.

Overlapping pull requests:

  • #2825 — resolving multi-address hostnames before connection.
  • #3011 and #3097 — IPv4-only WireGuard/AmneziaWG AllowedIPs handling.
  • #3018 — CIDR import regression coverage.
  • #3021 — batched Windows exclusion-route updates.

This PR keeps the split-tunneling changes together because the reproduced Windows failure crosses settings persistence, pre-connect DNS resolution, WireGuard/AmneziaWG configuration generation, and route application. Fixing only one layer did not restore reliable end-to-end bypass behavior in live testing.

Backward compatibility

  • Existing single-IP site settings remain readable.
  • Exported JSON retains the legacy ip property and adds/updates the complete ips array.
  • Split tunneling behavior is unchanged when site-based routing is disabled or the selected list contains no domain entries.
  • DNS failures retain the previously stored addresses instead of clearing them.

Verification

  • Built the Windows x64 client, service, and IFW installer in Release mode against dev at 94b51df2 (5.0.3.1).
  • Confirmed with git range-diff that the rebase preserves all three patch commits without changing their contents.

Live Windows checks were completed before this rebase with a real vpn:// profile and the same patch:

  • A large mixed exclusion list containing domains and IPv4 CIDRs.
  • Public multi-address services and a private hostname resolved through the Windows hosts file.
  • Excluded HTTPS sites and long-lived web connections remaining available while the VPN was connected.
  • Disconnect/reconnect behavior with refreshed domain addresses.

🔄 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/3030 **Author:** [@makkoar](https://github.com/makkoar) **Created:** 8/24/2026 **Status:** 🔄 Open **Base:** `dev` ← **Head:** `fix/windows-site-split-tunneling` --- ### 📝 Commits (3) - [`83977b2`](https://github.com/amnezia-vpn/amnezia-client/commit/83977b2908e41d1227beae402457327128a64a67) fix: refresh domain split tunneling routes - [`ab5c79b`](https://github.com/amnezia-vpn/amnezia-client/commit/ab5c79b3c7f718820bf5e0c062afd90606ac9c4e) fix: apply Windows exclusion routes transactionally - [`5dc9dd1`](https://github.com/amnezia-vpn/amnezia-client/commit/5dc9dd19ad44e1b09ff035aeeea8bd4489fc7244) fix: use type-safe route address copies ### 📊 Changes **13 files changed** (+956 additions, -196 deletions) <details> <summary>View changed files</summary> 📝 `client/core/controllers/ipSplitTunnelingController.cpp` (+129 -49) 📝 `client/core/controllers/ipSplitTunnelingController.h` (+11 -5) 📝 `client/core/repositories/secureAppSettingsRepository.cpp` (+44 -6) 📝 `client/core/repositories/secureAppSettingsRepository.h` (+5 -3) 📝 `client/daemon/daemon.cpp` (+66 -13) 📝 `client/daemon/daemon.h` (+3 -1) 📝 `client/daemon/wireguardutils.h` (+21 -1) 📝 `client/platforms/windows/daemon/windowsroutemonitor.cpp` (+241 -88) 📝 `client/platforms/windows/daemon/windowsroutemonitor.h` (+15 -2) 📝 `client/platforms/windows/daemon/wireguardutilswindows.cpp` (+161 -0) 📝 `client/platforms/windows/daemon/wireguardutilswindows.h` (+3 -0) 📝 `client/vpnConnection.cpp` (+228 -28) 📝 `client/vpnConnection.h` (+29 -0) </details> ### 📄 Description ## Summary This PR makes domain-based split tunneling more reliable for WireGuard and AmneziaWG on Windows. It keeps all current IPv4 addresses for each hostname, refreshes them before connection and reconnection, applies exclusion routes as a batch, and makes Windows route reconciliation resilient to duplicate and failed route operations. ## Problem I originally encountered two user-visible failures: - an IPv4 CIDR exclusion such as `x.x.x.0/24` could be accepted and remain visible in settings without receiving a working Windows bypass route; - domains and networks listed as exclusions could still be routed through the VPN tunnel, especially with IPv4-only WireGuard/AmneziaWG profiles. Site exclusions could become incomplete or unstable for several independent reasons: - a hostname could resolve to multiple IPv4 addresses, while stored and imported entries did not consistently retain the complete set; - cached addresses could be stale when a WireGuard/AmneziaWG tunnel was started or reconnected; - DNS resolution performed after tunnel activation could use a different route or resolver than the system and `hosts` configuration expected; - IPv4-only WireGuard configurations were incorrectly treated as incompatible with site split tunneling unless both `0.0.0.0/0` and `::/0` were present; - Windows exclusion routes were installed one by one while route-change notifications triggered repeated full reconciliations, which made large exclusion lists slow and prone to transient failures; - route installation errors were not propagated consistently, so activation could continue with a partially applied exclusion list even though every entry remained visible in settings; - repeated protocol setup could register duplicate reconnect handlers; These issues could result in slow page loads, intermittent connections, or local/private hostnames timing out while the VPN was connected, even though the same destinations worked without the VPN. ## Changes ### Domain entries and persistence - Normalize URLs and hostnames with `QUrl`, including IDN-to-ACE conversion and removal of a trailing dot. - Validate IPv4 CIDRs strictly and reject malformed domain names and imported addresses. - Store every unique IPv4 address associated with a hostname instead of a single address. - Merge duplicate entries during JSON import. - Export the full address list through `ips` while retaining the legacy `ip` field for backward compatibility. - Continue reading legacy settings where a site contains one string instead of a string list. - Associate asynchronous DNS results with the route mode in which the lookup was started, avoiding writes to a different list if the user changes the mode before the response arrives. ### DNS refresh before tunnel activation - Resolve domain exclusions before starting WireGuard or AmneziaWG on desktop platforms. - Use the system resolver, including local `hosts` mappings. - Limit resolution to 16 concurrent lookups and a total preparation timeout of 10 seconds. - Replace stored addresses only for domains that resolved successfully. - Keep the last known addresses for unresolved domains so a temporary DNS failure does not erase a working configuration. - Repeat the refresh on reconnect and cancel obsolete lookups on a new connection attempt, disconnect, or destruction. ### WireGuard and AmneziaWG configuration handling - Parse `AllowedIPs` consistently from JSON arrays, strings, and native configuration text, regardless of comma spacing. - Enable IPv4 site routes when `0.0.0.0/0` is present; `::/0` is no longer required for the currently IPv4-based site exclusion list. - Use unique queued IPC subscriptions so repeated protocol creation does not multiply reconnect callbacks. ### Windows route handling - Install exclusion routes as a batch and reuse one routing-table snapshot per address family. - Apply reference-count updates only after the batch succeeds. - Roll back newly added routes when activation or server switching fails. - Propagate route errors instead of continuing with a partially configured exclusion list. - Select and refresh the best physical interface and gateway for every exclusion route. - Treat an already existing equivalent route as usable without claiming ownership of it. - Skip duplicate and non-routable destinations. - Debounce bursts of `NotifyRouteChange2` events and safely handle notifications without a route row. - Avoid repeated attempts to clone the same conflicting captured route during one reconciliation pass. - Clean up only routes owned by the monitor. - For very large batches, temporarily pause `wcmSvc` route churn while applying the routes. This optimization is best-effort and falls back to normal batch installation if the required Windows APIs or privilege are unavailable. ## Related issues and overlapping pull requests Related issues: - [#2248](https://github.com/amnezia-vpn/amnezia-client/issues/2248) — slow and unstable Windows connection setup with large split-tunneling lists. - [#2907](https://github.com/amnezia-vpn/amnezia-client/issues/2907) — site split tunneling not applied to imported AmneziaWG profiles. - [#2939](https://github.com/amnezia-vpn/amnezia-client/issues/2939) — CIDR prefix handling for imported site entries. - [#3020](https://github.com/amnezia-vpn/amnezia-client/issues/3020) — quadratic Windows route setup for large CIDR exclusion lists. Overlapping pull requests: - [#2825](https://github.com/amnezia-vpn/amnezia-client/pull/2825) — resolving multi-address hostnames before connection. - [#3011](https://github.com/amnezia-vpn/amnezia-client/pull/3011) and [#3097](https://github.com/amnezia-vpn/amnezia-client/pull/3097) — IPv4-only WireGuard/AmneziaWG `AllowedIPs` handling. - [#3018](https://github.com/amnezia-vpn/amnezia-client/pull/3018) — CIDR import regression coverage. - [#3021](https://github.com/amnezia-vpn/amnezia-client/pull/3021) — batched Windows exclusion-route updates. This PR keeps the split-tunneling changes together because the reproduced Windows failure crosses settings persistence, pre-connect DNS resolution, WireGuard/AmneziaWG configuration generation, and route application. Fixing only one layer did not restore reliable end-to-end bypass behavior in live testing. ## Backward compatibility - Existing single-IP site settings remain readable. - Exported JSON retains the legacy `ip` property and adds/updates the complete `ips` array. - Split tunneling behavior is unchanged when site-based routing is disabled or the selected list contains no domain entries. - DNS failures retain the previously stored addresses instead of clearing them. ## Verification - Built the Windows x64 client, service, and IFW installer in Release mode against `dev` at [`94b51df2`](https://github.com/amnezia-vpn/amnezia-client/commit/94b51df24790bf52427afe82d81c87a95460bdfd) (`5.0.3.1`). - Confirmed with `git range-diff` that the rebase preserves all three patch commits without changing their contents. Live Windows checks were completed before this rebase with a real `vpn://` profile and the same patch: - A large mixed exclusion list containing domains and IPv4 CIDRs. - Public multi-address services and a private hostname resolved through the Windows `hosts` file. - Excluded HTTPS sites and long-lived web connections remaining available while the VPN was connected. - Disconnect/reconnect behavior with refreshed domain addresses. --- <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#3060
No description provided.