Skip to content

[macOS] Kill-switch blocks all egress on non-RFC1918 LANs; pinholes not re-installed when primary interface changes (Wi-Fi ↔ Ethernet) #5437

@ZaMpAdAKiNg

Description

@ZaMpAdAKiNg

[macOS] Kill-switch blocks all egress on non-RFC1918 LANs; pinholes not re-installed when primary interface changes (Wi-Fi ↔ Ethernet)


Summary

On macOS, NymVPN v2.23.2 with the (hardcoded "Always on") kill-switch and the Bypass LAN toggle enabled fails to function on a LAN that uses a non-RFC1918 address range delegated by a secondary router. Two distinct but related issues compound:

  1. Bypass LAN's whitelist is RFC1918-only and not user-configurable. Any DHCP-assigned address outside 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 falls through to the anchor's final block drop quick all rule, including ICMP/ARP-followup probes from the macOS reachability subsystem (SCNetworkReachability / "CrazyIvan46"). The macOS kernel then marks the LAN gateway "Not Reachable" and refuses to install a default route via that interface.

  2. Pinhole routes to the WireGuard entry gateways are not re-installed when the primary interface changes live. After connecting via Wi-Fi, the daemon installs /32 host routes to the entry IPs pointing at the Wi-Fi gateway. If the user later activates a USB-Ethernet adapter on a different LAN and the new adapter becomes Service Create LICENSE #1, the daemon does not regenerate those pinholes for the new underlay. Combined with the kill-switch (which keeps block drop quick all in PF regardless of app connection state), all egress halts.

Together these create a catch-22: the daemon can't bring the tunnel up over the new underlay because DNS / control-plane traffic is blocked by the very kill-switch that's waiting for the tunnel.

This appears related to but distinct from #2288 (closed, feature request) and #5244 (closed, sleep/wake variant of the same kill-switch design). The novel piece here is the live underlay-switch failure mode on non-RFC1918 LANs.


Environment

  • App: NymVPN.app v2.23.2 (nym-vpn-lib matches the version published 2026-05-26)
  • OS: macOS Sonoma 14.x, Apple Silicon
  • Daemon: system/net.nymtech.vpn.daemon (LaunchDaemon, root)
  • Mode: 2-hop WireGuard, kill-switch ("Always on"), Bypass LAN ON, IPv6 ON
  • Underlays in scope:
    • Wi-Fi en0, ISP router on an RFC1918 LAN (e.g. 192.168.x.x)
    • USB-Ethernet adapter (e.g. ASIX AX88179-based dock) as en5, plugged into a secondary router that hands out addresses in a non-RFC1918 range (the user's "isolated LAN") with its own gateway. Example used here: documentation range like 203.0.113.0/24 with gateway 203.0.113.1 — in the field this can be any operator-chosen prefix outside the three RFC1918 blocks.

Reproduction

  1. Connect NymVPN over Wi-Fi. Tunnel comes up cleanly; PF anchor nym is populated (~67 rules ending in block drop quick all).
  2. Plug in a USB-Ethernet adapter whose DHCP server (a secondary router under your control) hands out IPs in a non-RFC1918 range, e.g. 203.0.113.42/24 with router 203.0.113.1. The adapter becomes a network service; depending on Service Order it may immediately become Create LICENSE #1.
  3. From the macOS network stack's point of view, en5 is now the primary IPv4 interface and the kernel attempts reachability probes against 203.0.113.1.
  4. Observe: the probes never leave the interface. tcpdump -i en5 captures incoming ARP/IGMP/NetBIOS from the new gateway and the Mac's ARP replies, but zero ICMP echo-request frames are transmitted, even when the user invokes ping -b en5 203.0.113.1 directly. PF counter on block drop quick all increments by exactly the number of attempted outbound packets.
  5. route -n get -ifscope en5 203.0.113.1 initially shows <UP,HOST,DONE,LLINFO,WASCLONED,IFSCOPE,IFREF,ROUTER> then, within a couple of seconds, the ROUTER flag disappears. IPMonitor logs (subsystem == com.apple.SystemConfiguration) show reach: 0x00000000 (Not Reachable) against if_index N (en5) and the kernel demotes the IPv6 over the prior primary.
  6. The tunnel utun8 loses its underlay (the Wi-Fi default is also demoted). Disconnecting the app does not restore connectivity — the daemon keeps the anchor populated 24/7 (kill-switch design).
  7. The only paths back to a working state are: (a) disable the new Ethernet service, or (b) sudo launchctl bootout system/net.nymtech.vpn.daemon.

Expected behaviour

  • The kill-switch should not silently block reachability probes to the LAN gateway of any interface providing connectivity. Either:
  • When the primary interface changes, the daemon should:
    • Tear down stale entry-gateway /32 pinholes pointing at the old gateway, and
    • Install fresh pinholes via the new gateway, or
    • At minimum, briefly suspend the catch-all block so the new underlay can come up and the control plane (DNS, entry-gateway TCP/UDP) can flow.

Actual behaviour

  • block drop quick all (the final rule of anchor nym) accumulates packets at ~100/s while the new Ethernet service is active. Confirmed by reading pfctl -a nym -vsr.
  • The macOS kernel never installs a usable default route via the new interface because reachability fails.
  • nym-vpnd does not regenerate entry-gateway pinholes after primary changes.
  • No daemon log entries indicate that the failure is being detected.

Evidence (sanitized excerpts)

# PF anchor counter after ~30 min of attempted use with Ethernet primary
block drop quick all
  [ Evaluations: 404765    Packets: 404765    Bytes: ~400 MB  States: 0 ]

# tcpdump on en5 during ping attempt — only inbound from gateway, no ICMP out
<gateway-mac> > <mac-mac>: ARP Request who-has <mac-ip>
<mac-mac> > <gateway-mac>: ARP Reply <mac-ip> is-at <mac-mac>
... no ICMP echo request frames ...

# Route flags lose ROUTER after ~3s
route to: <gateway>
flags: <UP,HOST,DONE,LLINFO,WASCLONED,IFSCOPE,IFREF,ROUTER>  ← initially
flags: <UP,HOST,DONE,LLINFO,WASCLONED,IFSCOPE,IFREF>          ← after probes drop

# Validated workaround: with daemon stopped, the same Ethernet primary works fully
ping <gateway>     : 100% received
ping 8.8.8.8       : 100% received
curl https://api.ipify.org : HTTP 200

Workarounds in use

Locally we ship two scripts (paraphrased):

  • nym-cable-on: launchctl bootout the daemon, pfctl -a nym -F all, reorder Service Order to put the Ethernet adapter first, enable it. Daemon stays stopped — Internet flows directly via the new LAN, without VPN.
  • nym-cable-off: stop daemon, reorder to Wi-Fi first, disable Ethernet, launchctl bootstrap the daemon, open the app for manual Connect.

These are pragmatic but defeat the purpose of running NymVPN at the user's primary workspace.


Proposed fixes (in order of impact)

  1. User-configurable Bypass LAN allowlist accepting CIDR (revisit Allow Access To LAN When Firewall/Kill Switch Is Enabled Setting #2288). When enabled, install pass in/out quick inet from any to <cidr> rules in the anchor ahead of the catch-all block. This alone unblocks the reachability probes and lets non-RFC1918 LANs work.
  2. Auto-bypass the current DHCP-assigned LAN of every active service for the minimal protocols required by reachability detection (ICMP echo, ARP, ND6, DHCP). This avoids users having to configure anything.
  3. Re-install entry-gateway pinholes on primary-interface change. Subscribe to State:/Network/Global/IPv4 via SCDynamicStore (or NEHelper events) and regenerate /32 host routes accordingly.
  4. Optionally provide a "kill-switch: off" toggle in the UI (or at least a "soft" mode that lifts the catch-all block when the user explicitly disconnects). Today the kill-switch is hardcoded "Always on" without a UI control.

(1) + (3) together would fix our case completely.


Related

Happy to provide additional sanitized logs, daemon traces, or to test patches on a 2-hop WireGuard build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions