[GH-ISSUE #3240] [macOS][AmneziaWG] Hostnames in site-based split tunneling are never resolved — no routes are installed, whitelist mode silently does nothing #1502

Open
opened 2026-10-13 20:43:54 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Liu-Lin-Hai on GitHub (Sep 28, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3240

Summary

On macOS, site-based split tunneling ("基于网站的隧道分离") is completely non-functional for hostname entries. The client never resolves them to IP addresses, so no routes are ever installed for them.
The list is stored correctly, the mode is applied (the full-tunnel 0/1 + 128.0/1 routes are removed as expected), but every hostname entry keeps an empty IP list, so nothing gets routed into the tunnel. In whitelist mode ("Only the sites listed here will be accessed via VPN") this means the VPN carries no traffic at all except for entries that happen to be raw IP addresses — all hostnames fall through to the direct/default path, silently leaking traffic outside the VPN.
Raw IP entries (e.g. 1.1.1.1) work correctly and do get routes. Only hostnames are affected.

Environment

  • AmneziaVPN 5.0.1 (build 5), official macOS package
  • macOS 15.5 (24F74), arm64
  • Protocol: AmneziaWG (self-hosted amnezia-awg2 container)
  • Server: self-hosted, Ubuntu 24.04, AmneziaWG 3.1 parameters
  • Site-based split tunneling: enabled
  • App-based split tunneling: disabled

Steps to reproduce

  1. Connect to a self-hosted AmneziaWG server once to create the profile.
  2. Disconnect.
  3. Open site-based split tunneling, select mode "Only the sites listed here will be accessed via VPN" (whitelist).
  4. Add a hostname entry, e.g. google.com, using the Add button. (Adding while connected is rejected by the UI, so this is done disconnected.)
  5. Add a raw IP entry, e.g. 1.1.1.1, the same way.
  6. Connect.
  7. Inspect netstat -rn -f inet and the stored configuration.

Expected behavior

  • After connecting, google.com should be resolved and a route for its address(es) should be installed pointing at the tunnel interface.
  • The whitelist should route the listed hostnames through the VPN and leave everything else direct.

Actual behavior

  • google.com gets no route. Its stored IP list stays empty.
  • 1.1.1.1 gets a proper host route into the tunnel.
  • Because the full-tunnel /1 routes are removed when the site list is active, all hostname traffic goes direct — including traffic the user explicitly asked to send through the VPN.

Stored configuration (~/Library/Preferences/org.amneziavpn.AmneziaVPN.plist)

Conf.routeMode                    = 1        (whitelist: "only the listed sites via VPN")
Conf.sitesSplitTunnelingEnabled   = True
Conf.ForwardSites                 = 656 entries
entries with a non-empty IP list  = 1        ← only the raw IP entry
  "1.1.1.1"        → (route installed, see below)
  "google.com"     → []
  "chatgpt.com"    → []
  "claude.ai"      → []
  (653 further hostname entries, all [])

Conf.ExceptSites behaves identically: 94 hostname entries, 0 with a non-empty IP list.

Route table while connected

$ netstat -rn -f inet | grep utun10
1.0.0.1/32         utun10
1.1.1.1/32         utun10
10.8.1.1           utun10

Note that 0/1 and 128.0/1 are correctly absent (the mode is applied), and the only routes present are the two raw-IP entries. There is no route for any hostname.

$ dig +short google.com
142.250.23.113
$ route -n get 142.250.23.113
   interface: en0            ← direct, not the tunnel
$ curl -o /dev/null -w '%{http_code}' https://www.google.com
000                            ← fails, direct connection is blocked

Exit IP while in this state is the client's real ISP address, not the VPN server's.

Evidence that this is not a DNS/environment problem

The system resolver works correctly while connected:

$ dig +short google.com
142.250.23.113
$ dig +short chatgpt.com
104.18.32.47

So the client has working name resolution available but does not use it to populate the entries.

Reproduced through every input path

How the entry was added Result
Imported from JSON ([{"hostname": "google.com", "ip": ""}]) — 656 entries 0 entries with IPs, 0 routes
Imported from JSON with the ip field pre-filled with a resolved address 0 entries with IPs, 0 routes
Added through the Add button in the UI, while disconnected 0 entries with IPs, 0 routes
Added through the Add button, while connected worked once — see below

The one case that worked

While connected, adding ipify.org through the UI did populate the IP list and install routes:

"ipify.org" → ['172.67.74.152', '104.26.12.205', '104.26.13.205']

and three matching /32 routes appeared on the tunnel interface.
This suggests site resolution is only attempted under some condition that is not met in the normal flow (for example, resolution is attempted during connection setup before the system resolver is usable, and is skipped silently on failure — the GUI reports "Connected" regardless).
After that single success, subsequent UI additions (while disconnected) and imports never resolved again. The one resolved entry was later lost when the list was re-imported.

Impact

Whitelist mode is the natural configuration for users who want domestic traffic to stay direct and only specific foreign sites to go through the VPN. With this defect it cannot be used at all:

  • No hostname ever reaches the tunnel, so the VPN provides no access to the sites the user configured.
  • The /1 routes have already been removed, so the user's real IP is exposed to everything — worse than not enabling split tunneling at all.
    Blacklist mode ("All sites except the listed ones") is equally affected: the exclusion entries get no routes, so the exclusions do nothing.
  • #2937 — IpSplitTunnelingController::normalizeHostname strips CIDR suffixes; touches the same input path but is a different defect (our entries are plain hostnames with no prefix).
  • #3104 — request to allow toggling split tunneling while connected.
  • #2985 — CIDR prefixes dropped on export (Windows).
    None of these cover hostnames never being resolved.

Suggested investigation

  • client/core/controllers/ipSplitTunnelingController.cpp — where entries are turned into routes; check the hostname→IP step and whether failures are logged or silently swallowed.
  • The call path used by importSitesFromJson versus the UI "Add" handler — they appear to differ in whether resolution runs at all.
  • Whether resolution is attempted before DnsUtils/the system resolver is usable during connection setup.
  • Consider surfacing a visible error (or a failed-entry marker in the list UI) when a hostname cannot be resolved, instead of reporting "Connected" and dropping the route.

Notes

No logs are attached because the app reports no error at any point — the failure is entirely silent. Happy to collect logs or run any build with extra diagnostics if that would help.

Originally created by @Liu-Lin-Hai on GitHub (Sep 28, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3240 ## Summary On macOS, site-based split tunneling ("基于网站的隧道分离") is completely non-functional for **hostname** entries. The client never resolves them to IP addresses, so no routes are ever installed for them. The list is stored correctly, the mode is applied (the full-tunnel `0/1` + `128.0/1` routes are removed as expected), but every hostname entry keeps an empty IP list, so nothing gets routed into the tunnel. In whitelist mode ("Only the sites listed here will be accessed via VPN") this means **the VPN carries no traffic at all except for entries that happen to be raw IP addresses** — all hostnames fall through to the direct/default path, silently leaking traffic outside the VPN. Raw IP entries (e.g. `1.1.1.1`) work correctly and do get routes. Only hostnames are affected. ## Environment - AmneziaVPN 5.0.1 (build 5), official macOS package - macOS 15.5 (24F74), arm64 - Protocol: AmneziaWG (self-hosted `amnezia-awg2` container) - Server: self-hosted, Ubuntu 24.04, AmneziaWG 3.1 parameters - Site-based split tunneling: enabled - App-based split tunneling: disabled ## Steps to reproduce 1. Connect to a self-hosted AmneziaWG server once to create the profile. 2. Disconnect. 3. Open site-based split tunneling, select mode **"Only the sites listed here will be accessed via VPN"** (whitelist). 4. Add a hostname entry, e.g. `google.com`, using the **Add** button. (Adding while connected is rejected by the UI, so this is done disconnected.) 5. Add a raw IP entry, e.g. `1.1.1.1`, the same way. 6. Connect. 7. Inspect `netstat -rn -f inet` and the stored configuration. ## Expected behavior - After connecting, `google.com` should be resolved and a route for its address(es) should be installed pointing at the tunnel interface. - The whitelist should route the listed hostnames through the VPN and leave everything else direct. ## Actual behavior - `google.com` gets **no** route. Its stored IP list stays empty. - `1.1.1.1` gets a proper host route into the tunnel. - Because the full-tunnel `/1` routes are removed when the site list is active, all hostname traffic goes **direct** — including traffic the user explicitly asked to send through the VPN. ### Stored configuration (`~/Library/Preferences/org.amneziavpn.AmneziaVPN.plist`) ``` Conf.routeMode = 1 (whitelist: "only the listed sites via VPN") Conf.sitesSplitTunnelingEnabled = True Conf.ForwardSites = 656 entries entries with a non-empty IP list = 1 ← only the raw IP entry "1.1.1.1" → (route installed, see below) "google.com" → [] "chatgpt.com" → [] "claude.ai" → [] (653 further hostname entries, all []) ``` `Conf.ExceptSites` behaves identically: 94 hostname entries, **0** with a non-empty IP list. ### Route table while connected ``` $ netstat -rn -f inet | grep utun10 1.0.0.1/32 utun10 1.1.1.1/32 utun10 10.8.1.1 utun10 ``` Note that `0/1` and `128.0/1` are correctly absent (the mode is applied), and the only routes present are the two raw-IP entries. There is no route for any hostname. ``` $ dig +short google.com 142.250.23.113 $ route -n get 142.250.23.113 interface: en0 ← direct, not the tunnel $ curl -o /dev/null -w '%{http_code}' https://www.google.com 000 ← fails, direct connection is blocked ``` Exit IP while in this state is the client's real ISP address, not the VPN server's. ## Evidence that this is not a DNS/environment problem The system resolver works correctly while connected: ``` $ dig +short google.com 142.250.23.113 $ dig +short chatgpt.com 104.18.32.47 ``` So the client has working name resolution available but does not use it to populate the entries. ## Reproduced through every input path | How the entry was added | Result | |---|---| | Imported from JSON (`[{"hostname": "google.com", "ip": ""}]`) — 656 entries | 0 entries with IPs, 0 routes | | Imported from JSON with the `ip` field **pre-filled** with a resolved address | 0 entries with IPs, 0 routes | | Added through the **Add** button in the UI, while disconnected | 0 entries with IPs, 0 routes | | Added through the **Add** button, while **connected** | **worked once** — see below | ### The one case that worked While connected, adding `ipify.org` through the UI **did** populate the IP list and install routes: ``` "ipify.org" → ['172.67.74.152', '104.26.12.205', '104.26.13.205'] ``` and three matching `/32` routes appeared on the tunnel interface. This suggests site resolution is only attempted under some condition that is not met in the normal flow (for example, resolution is attempted during connection setup before the system resolver is usable, and is skipped silently on failure — the GUI reports "Connected" regardless). After that single success, subsequent UI additions (while disconnected) and imports never resolved again. The one resolved entry was later lost when the list was re-imported. ## Impact Whitelist mode is the natural configuration for users who want domestic traffic to stay direct and only specific foreign sites to go through the VPN. With this defect it cannot be used at all: - No hostname ever reaches the tunnel, so the VPN provides no access to the sites the user configured. - The `/1` routes have already been removed, so the user's real IP is exposed to everything — worse than not enabling split tunneling at all. Blacklist mode ("All sites except the listed ones") is equally affected: the exclusion entries get no routes, so the exclusions do nothing. ## Related issues - #2937 — `IpSplitTunnelingController::normalizeHostname` strips CIDR suffixes; touches the same input path but is a different defect (our entries are plain hostnames with no prefix). - #3104 — request to allow toggling split tunneling while connected. - #2985 — CIDR prefixes dropped on export (Windows). None of these cover hostnames never being resolved. ## Suggested investigation - `client/core/controllers/ipSplitTunnelingController.cpp` — where entries are turned into routes; check the hostname→IP step and whether failures are logged or silently swallowed. - The call path used by `importSitesFromJson` versus the UI "Add" handler — they appear to differ in whether resolution runs at all. - Whether resolution is attempted before `DnsUtils`/the system resolver is usable during connection setup. - Consider surfacing a visible error (or a failed-entry marker in the list UI) when a hostname cannot be resolved, instead of reporting "Connected" and dropping the route. ## Notes No logs are attached because the app reports no error at any point — the failure is entirely silent. Happy to collect logs or run any build with extra diagnostics if that would help.
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#1502
No description provided.