[GH-ISSUE #3106] When VPN enabled and attempt connecting to wifi, the captive portal page doesn't appear. #1439

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

Originally created by @mvcl on GitHub (Aug 31, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3106

In using AmneziaWG I found a sub-optimal experience. I'd like to report it in the hope that it may lead to certain improvements.

I was on an SAS long-haul flight on one of their A350's which is still using their pre-starlink/older satellite internet provider. Connecting to the inflight wifi requires interacting with BOTH their captive portal AND a commissioning web page from the surf.flysas.com domain. When the AmneziaWG (current version, MacOS) is engaged, NEITHER of these appears, so the wifi on the plane was a total FAIL unless and until AmneziaWG VPN is switched off from the menu. BOTH the portal and then the commissioning/sign-in/payment pages from surf.flysas.com MUST WORK BEFORE AmneziaWG VPN can be turned on.

Thinking towards possible solutions, if you implemented a "pilot light" for IP connectivity as a requirement for enabling the VPN tunneling, then when IP is not being routed, the captive portal and SAS.com commissioning pages would work. It's only when the commissioning is completed successfully that IP starts to be routed. And accordingly at that point and only then, your Amnezia tunnel could see the pilot IP packets routing, and enable the tunneling of further traffic through the VPN.

Client Environment: MacOS Tahoe 26.6.2 or .3 on MacBook Air 13"

Originally created by @mvcl on GitHub (Aug 31, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/3106 In using AmneziaWG I found a sub-optimal experience. I'd like to report it in the hope that it may lead to certain improvements. I was on an SAS long-haul flight on one of their A350's which is still using their pre-starlink/older satellite internet provider. Connecting to the inflight wifi requires interacting with BOTH their captive portal AND a commissioning web page from the surf.flysas.com domain. When the AmneziaWG (current version, MacOS) is engaged, NEITHER of these appears, so the wifi on the plane was a total FAIL unless and until AmneziaWG VPN is switched off from the menu. BOTH the portal and then the commissioning/sign-in/payment pages from surf.flysas.com MUST WORK BEFORE AmneziaWG VPN can be turned on. Thinking towards possible solutions, if you implemented a "pilot light" for IP connectivity as a requirement for enabling the VPN tunneling, then when IP is not being routed, the captive portal and SAS.com commissioning pages would work. It's only when the commissioning is completed successfully that IP starts to be routed. And accordingly at that point and only then, your Amnezia tunnel could see the pilot IP packets routing, and enable the tunneling of further traffic through the VPN. Client Environment: MacOS Tahoe 26.6.2 or .3 on MacBook Air 13"
Author
Owner

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

I checked this against the macOS NetworkExtension path in the 5.0.1.5 source and current dev.

I think the reporter's idea of waiting for usable underlay connectivity is directionally correct, but a single “pilot IP packet” is probably not sufficient.

There appears to be a more specific routing/bootstrap problem here.

For the macOS NetworkExtension build, full-tunnel configuration explicitly uses:

tunnelProtocol.includeAllNetworks = NO;

if (splitTunnelType == 0) {
    tunnelProtocol.enforceRoutes = YES;

    if (@available(iOS 14.2, macOS 11.0, *)) {
        tunnelProtocol.excludeLocalNetworks = YES;
    }
}

This is present in the 5.0.1.5 source and remains present in current dev.

For WireGuard/AWG, PacketTunnelSettingsGenerator then converts peer allowedIPs into includedRoutes.

So a normal:

0.0.0.0/0
::/0

configuration becomes an enforced route-based full tunnel.

The automatically generated excludedRoutes are approximately:

resolved VPN endpoint(s)
+
explicit peer excludeIPs

This matters for captive networks.

Apple documents a special captive-portal exception for includeAllNetworks: captive portal negotiation traffic is always excluded from the tunnel.

But Amnezia's macOS NE configuration deliberately does not use includeAllNetworks; it uses:

includeAllNetworks = false
enforceRoutes      = true
excludeLocalNetworks = true

Apple describes enforceRoutes differently: included routes are scoped to the VPN and excluded routes are scoped to the current primary interface. It also supersedes application interface-scoping.

Therefore an ordinary remote web page that is required to commission the Wi-Fi is a problematic case.

The SAS report is especially useful because authorization is explicitly two-stage:

1. captive portal
2. commissioning/sign-in/payment page at surf.flysas.com
3. normal Internet access
4. only then can the VPN server actually be useful

Even if macOS successfully recognizes and bypasses the first stage as special captive-portal negotiation traffic, the second stage is ordinary web traffic.

surf.flysas.com is not inherently:

the VPN endpoint
a local-network destination
an explicit excludedRoute

so with an enforced 0.0.0.0/0 route it can remain scoped to the tunnel.

That creates a bootstrap cycle:

Wi-Fi association succeeds
        ↓
Wi-Fi requires portal/commissioning
        ↓
commissioning traffic needs physical underlay
        ↓
full-tunnel route enforcement sends ordinary remote traffic to VPN
        ↓
VPN cannot become useful because underlay is not commissioned
        ↓
commissioning cannot complete because its traffic is inside the unusable VPN

Turning the VPN off breaks the cycle, which matches the reported behavior.

I would therefore model this as an explicit state-machine problem rather than only adding a generic Internet boolean:

UNDERLAY_DOWN

UNDERLAY_LINK_UP
    Wi-Fi associated
    DHCP/address/gateway available

UNDERLAY_AUTH_REQUIRED
    HTTP probe intercepted/redirected
    or captive authorization otherwise detected

UNDERLAY_AUTHORIZING
    captive/commissioning traffic must remain on physical path

UNDERLAY_READY
    external HTTP reachability proven on physical underlay

VPN_BOOTSTRAPPING
    VPN endpoint/DNS/transport being established

VPN_READY
    tunnel readiness/handshake proven

The important invariant would be:

UNDERLAY_AUTH_REQUIRED
or
UNDERLAY_AUTHORIZING

    =>

do not enforce the normal full-tunnel route over traffic
required to authorize the physical network

A single “pilot IP packet” should not be allowed to prove UNDERLAY_READY.

For example:

Wi-Fi associated
    != Internet usable

gateway ping works
    != captive authorization complete

DNS query succeeds
    != Internet usable

ICMP succeeds
    != HTTP/HTTPS usable

HTTP redirect/interception
    -> evidence for CAPTIVE_REQUIRED

expected HTTP response over physical path
    -> stronger evidence for UNDERLAY_READY

WireGuard handshake
    -> VPN liveness only

This is also important because QNetworkInformation is not enough to represent these states on macOS. On Apple platforms its reachability is effectively limited to:

Online
Offline
Unknown

so a binary hasInternetAccess abstraction cannot reliably distinguish:

no network
Wi-Fi associated but captive
DNS failure
walled garden
ordinary Internet available
VPN server unreachable
VPN tunnel not ready

I would make the underlay probe explicitly path-bound.

However, with enforceRoutes = true, Apple documents that the enforced route policy supersedes application interface scoping. So the safest ordering may be:

physical network change detected
        ↓
temporarily leave/suspend full-tunnel enforcement
        ↓
probe/authenticate physical underlay
        ↓
if captive:
    expose normal captive/commissioning flow
    remain WAITING_FOR_UNDERLAY_AUTHORIZATION
        ↓
underlay external reachability becomes stable
        ↓
start/reassert VPN
        ↓
verify VPN readiness
        ↓
enable final full-tunnel enforcement

rather than trying to run a Wi-Fi-bound probe after the enforced 0/0 route has already been committed.

I would not simply change:

enforceRoutes = true

back to:

includeAllNetworks = true

without checking why the current route-based design was chosen. That may restore Apple's documented captive-portal exception but reintroduce whatever leak/routing behavior enforceRoutes was intended to fix.

Instead I would make captive bootstrap an explicit lifecycle phase.

A useful regression matrix would be:

1. Wi-Fi has DHCP but captive HTTP redirect
   expected:
       UNDERLAY_AUTH_REQUIRED
       VPN full-tunnel enforcement deferred

2. ICMP works but HTTP is captive/intercepted
   expected:
       not UNDERLAY_READY

3. DNS works but HTTP is captive
   expected:
       not UNDERLAY_READY

4. captive portal itself is local, but a required commissioning page
   is an external domain
   expected:
       both remain reachable through physical underlay until authorization

5. authorization completes
   expected:
       stable physical-underlay HTTP success
       -> VPN bootstrap
       -> tunnel readiness
       -> final enforced routes

6. normal Internet works but VPN endpoint is unreachable
   expected:
       VPN_BOOTSTRAP_FAILED
       not CAPTIVE_REQUIRED

7. WireGuard handshake succeeds but tunnel data path is unusable
   expected:
       tunnel not treated as fully ready from handshake alone

8. Strict Kill Switch enabled
   expected:
       explicitly documented strict behavior;
       do not accidentally give ordinary full-tunnel mode the same semantics

I think the strongest invariant for this issue is:

PHYSICAL_NETWORK_REQUIRES_AUTHORIZATION
    =>
VPN POLICY MUST NOT PREVENT
PHYSICAL_NETWORK_AUTHORIZATION

and only after underlay authorization is proven should the normal full-tunnel policy become authoritative.

That would solve both halves of the SAS report: not only the initial captive portal, but also the separate commissioning/sign-in/payment web flow that has to complete before usable Internet exists.

<!-- gh-comment-id:5588913135 --> @HeisLuka commented on GitHub (Sep 8, 2026): I checked this against the macOS NetworkExtension path in the `5.0.1.5` source and current `dev`. I think the reporter's idea of waiting for usable underlay connectivity is directionally correct, but a single “pilot IP packet” is probably not sufficient. There appears to be a more specific routing/bootstrap problem here. For the macOS NetworkExtension build, full-tunnel configuration explicitly uses: ```cpp tunnelProtocol.includeAllNetworks = NO; if (splitTunnelType == 0) { tunnelProtocol.enforceRoutes = YES; if (@available(iOS 14.2, macOS 11.0, *)) { tunnelProtocol.excludeLocalNetworks = YES; } } ``` This is present in the `5.0.1.5` source and remains present in current `dev`. For WireGuard/AWG, `PacketTunnelSettingsGenerator` then converts peer `allowedIPs` into `includedRoutes`. So a normal: ```text 0.0.0.0/0 ::/0 ``` configuration becomes an enforced route-based full tunnel. The automatically generated `excludedRoutes` are approximately: ```text resolved VPN endpoint(s) + explicit peer excludeIPs ``` This matters for captive networks. Apple documents a special captive-portal exception for `includeAllNetworks`: captive portal negotiation traffic is always excluded from the tunnel. But Amnezia's macOS NE configuration deliberately does not use `includeAllNetworks`; it uses: ```text includeAllNetworks = false enforceRoutes = true excludeLocalNetworks = true ``` Apple describes `enforceRoutes` differently: included routes are scoped to the VPN and excluded routes are scoped to the current primary interface. It also supersedes application interface-scoping. Therefore an ordinary remote web page that is required to commission the Wi-Fi is a problematic case. The SAS report is especially useful because authorization is explicitly two-stage: ```text 1. captive portal 2. commissioning/sign-in/payment page at surf.flysas.com 3. normal Internet access 4. only then can the VPN server actually be useful ``` Even if macOS successfully recognizes and bypasses the first stage as special captive-portal negotiation traffic, the second stage is ordinary web traffic. `surf.flysas.com` is not inherently: ```text the VPN endpoint a local-network destination an explicit excludedRoute ``` so with an enforced `0.0.0.0/0` route it can remain scoped to the tunnel. That creates a bootstrap cycle: ```text Wi-Fi association succeeds ↓ Wi-Fi requires portal/commissioning ↓ commissioning traffic needs physical underlay ↓ full-tunnel route enforcement sends ordinary remote traffic to VPN ↓ VPN cannot become useful because underlay is not commissioned ↓ commissioning cannot complete because its traffic is inside the unusable VPN ``` Turning the VPN off breaks the cycle, which matches the reported behavior. I would therefore model this as an explicit state-machine problem rather than only adding a generic Internet boolean: ```text UNDERLAY_DOWN UNDERLAY_LINK_UP Wi-Fi associated DHCP/address/gateway available UNDERLAY_AUTH_REQUIRED HTTP probe intercepted/redirected or captive authorization otherwise detected UNDERLAY_AUTHORIZING captive/commissioning traffic must remain on physical path UNDERLAY_READY external HTTP reachability proven on physical underlay VPN_BOOTSTRAPPING VPN endpoint/DNS/transport being established VPN_READY tunnel readiness/handshake proven ``` The important invariant would be: ```text UNDERLAY_AUTH_REQUIRED or UNDERLAY_AUTHORIZING => do not enforce the normal full-tunnel route over traffic required to authorize the physical network ``` A single “pilot IP packet” should not be allowed to prove `UNDERLAY_READY`. For example: ```text Wi-Fi associated != Internet usable gateway ping works != captive authorization complete DNS query succeeds != Internet usable ICMP succeeds != HTTP/HTTPS usable HTTP redirect/interception -> evidence for CAPTIVE_REQUIRED expected HTTP response over physical path -> stronger evidence for UNDERLAY_READY WireGuard handshake -> VPN liveness only ``` This is also important because `QNetworkInformation` is not enough to represent these states on macOS. On Apple platforms its reachability is effectively limited to: ```text Online Offline Unknown ``` so a binary `hasInternetAccess` abstraction cannot reliably distinguish: ```text no network Wi-Fi associated but captive DNS failure walled garden ordinary Internet available VPN server unreachable VPN tunnel not ready ``` I would make the underlay probe explicitly path-bound. However, with `enforceRoutes = true`, Apple documents that the enforced route policy supersedes application interface scoping. So the safest ordering may be: ```text physical network change detected ↓ temporarily leave/suspend full-tunnel enforcement ↓ probe/authenticate physical underlay ↓ if captive: expose normal captive/commissioning flow remain WAITING_FOR_UNDERLAY_AUTHORIZATION ↓ underlay external reachability becomes stable ↓ start/reassert VPN ↓ verify VPN readiness ↓ enable final full-tunnel enforcement ``` rather than trying to run a Wi-Fi-bound probe after the enforced `0/0` route has already been committed. I would not simply change: ```text enforceRoutes = true ``` back to: ```text includeAllNetworks = true ``` without checking why the current route-based design was chosen. That may restore Apple's documented captive-portal exception but reintroduce whatever leak/routing behavior `enforceRoutes` was intended to fix. Instead I would make captive bootstrap an explicit lifecycle phase. A useful regression matrix would be: ```text 1. Wi-Fi has DHCP but captive HTTP redirect expected: UNDERLAY_AUTH_REQUIRED VPN full-tunnel enforcement deferred 2. ICMP works but HTTP is captive/intercepted expected: not UNDERLAY_READY 3. DNS works but HTTP is captive expected: not UNDERLAY_READY 4. captive portal itself is local, but a required commissioning page is an external domain expected: both remain reachable through physical underlay until authorization 5. authorization completes expected: stable physical-underlay HTTP success -> VPN bootstrap -> tunnel readiness -> final enforced routes 6. normal Internet works but VPN endpoint is unreachable expected: VPN_BOOTSTRAP_FAILED not CAPTIVE_REQUIRED 7. WireGuard handshake succeeds but tunnel data path is unusable expected: tunnel not treated as fully ready from handshake alone 8. Strict Kill Switch enabled expected: explicitly documented strict behavior; do not accidentally give ordinary full-tunnel mode the same semantics ``` I think the strongest invariant for this issue is: ```text PHYSICAL_NETWORK_REQUIRES_AUTHORIZATION => VPN POLICY MUST NOT PREVENT PHYSICAL_NETWORK_AUTHORIZATION ``` and only after underlay authorization is proven should the normal full-tunnel policy become authoritative. That would solve both halves of the SAS report: not only the initial captive portal, but also the separate commissioning/sign-in/payment web flow that has to complete before usable Internet exists.
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#1439
No description provided.