macOS: support multiple simultaneous tunnels - #63
Open
cremenescu wants to merge 10 commits into
Open
Conversation
The ctl_info and sockaddr_ctl definitions use u_char, u_int16_t and u_int32_t without including <sys/types.h>. Newer clang versions with strict module verification (Xcode 16 and later) refuse to build the module because these declarations must be imported before use. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
A tunnel in the .restarting state is automatically reactivated by the status observer when its connection reaches .disconnected. If the user asks for deactivation while the restart is still pending, the tunnel would go down and immediately come back up. Reset the status from the actual connection status before stopping, so an explicit deactivation request always wins over a pending restart. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
The adapter located its tunnel device by scanning the process's file descriptors and taking the first utun control socket found. With a single tunnel per extension process that was always the right one, but macOS runs multiple simultaneous tunnel sessions in one extension process: the second adapter would attach to the first tunnel's utun, leaving its own device without a reader. The second tunnel then passed no traffic at all, while the first suffered from two backends competing for the same descriptor. Collect all utun control sockets instead, exclude the ones already claimed by other adapters in the process, and prefer the device that already carries this tunnel's addresses, which setTunnelNetworkSettings has assigned by the time the backend starts. The claim is released if the backend fails to start, on stop, and on deinit. This makes simultaneous tunnels actually pass traffic on macOS. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
On iOS the OS allows only one active VPN configuration at a time, so activating a tunnel first parks it in the .waiting state, deactivates the tunnel currently in operation, and completes the activation once the latter is fully down. On macOS there is no such OS-level restriction: each tunnel is its own NETunnelProviderManager session and multiple sessions can be active simultaneously. Make the deactivate-then-activate handoff iOS-only, so that on macOS activating a tunnel no longer tears down the tunnels already in operation. Also add tunnelsInOperation(), returning every non-inactive tunnel, for use by the macOS UI. On multi-user systems the tunnels list deliberately retains other users' tunnels; those are excluded, since they are not this user's to display or deactivate. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
stopTunnel() works around Apple bug 32073323 by calling exit(0) on macOS. If the OS runs multiple simultaneous tunnel sessions in a single extension process, exiting on the first stop would tear down the remaining sessions too. Keep a process-wide count of tunnels and only exit once the last one is being stopped. The count covers the whole duration of a start attempt - the adapter can spend seconds in DNS resolution and interface setup - so that a tunnel stopping meanwhile doesn't consider itself the last one in the process and kill a concurrent activation; the failure paths of startTunnel() remove the tunnel from the count again. When each session runs in its own process, the count is always 0 or 1 and the behavior is unchanged. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
Now that TunnelsManager no longer serializes tunnel activation on macOS, generalize the menu bar UI from tracking a single current tunnel to tracking every tunnel in operation: - TunnelsTracker publishes the list of non-inactive tunnels instead of a single currentTunnel slot. - StatusMenu keeps the existing presentation when zero or one tunnel is in operation. With more than one, the status line shows the number of tunnels and the Deactivate item becomes Deactivate All, which deactivates every tunnel in operation, including tunnels that are still activating or restarting. - StatusItemController shows the active icon when all tunnels in operation are active, and the progress animation while any of them is transitioning. - AppDelegate's quit and App Store update alerts enumerate all active tunnels rather than just one; the App Store alert asks to disable on-demand only for the tunnels that actually have it enabled. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
When a tunnel lists DNS search domains (the non-IP entries of the wg-quick DNS= line), the tunnel's DNS servers have so far always been installed as the system-wide resolver (match domains [""]) while the search domains only served for completing short names. With multiple simultaneous tunnels this makes concurrently active tunnels fight over system DNS, and there was no way to use a tunnel's DNS only for the internal domains behind that tunnel. Add a per-tunnel Split DNS toggle, stored in the provider configuration alongside the existing keys so the wg-quick config text remains untouched and portable. When enabled and the tunnel has search domains, the DNS servers are installed with the search domains as match domains, so only queries for those domains go through the tunnel; everything else stays on the system resolver. The default remains the current match-everything behavior, so existing full-tunnel setups keep their leak-free DNS. The toggle is exposed as a checkbox in the macOS tunnel editor and as a switch in the iOS tunnel editor, and toggling it restarts an active tunnel the same way a configuration edit does. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
WireGuard runs as a menu bar accessory app. When an error alert has no source window to attach to as a sheet - e.g. an activation failure while the Manage Tunnels window is closed - it falls back to NSAlert.runModal(). Shown while the app is inactive, the modal window comes up behind everything with no way to reach it, and the modal loop freezes the whole app until it is dismissed. Activate the app first so the alert is actually visible. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
TunnelEditViewController.populateFields() force-unwraps the tunnel's configuration. When the saved configuration cannot be read - for instance when its keychain entry is inaccessible - that configuration is nil and opening the editor traps. Check that the configuration is readable before presenting the editor and report the failure instead. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
On iOS a network change sets the peer endpoints again before bumping the sockets, but on macOS the path update handler only bumps the sockets, leaving whatever endpoint each peer currently has. That breaks tunnels across a network change in two ways. A peer configured with a hostname keeps the address the hostname resolved to on the previous network, so a dynamic DNS record that moved, or a name that resolves differently per network, is never picked up. Worse, a peer that roamed keeps the roamed address. A server reached at an address that only exists on one network - its address inside another tunnel, or on the LAN it is hosted on - leaves the peer pinned to that address once the machine moves, and every handshake then fails with 'no route to host' until the tunnel is deactivated and activated again. Set the endpoints of all peers again after a network change, re-resolving the ones configured with a hostname. Resolution failures keep the address the hostname last resolved to, so a network without working DNS does not tear the endpoints down. The work is delayed and coalesced, since a single network change produces a burst of path updates and resolution blocks the queue. Peers configured with a literal address need no resolution at all. Signed-off-by: Razvan Cremenescu <razvan.cremenescu@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
macOS, unlike iOS, can run multiple packet tunnel provider sessions at the same time, but the app serializes activation on both platforms: activating one tunnel deactivates the tunnel already in operation. On iOS that mirrors an OS restriction. On macOS it is purely app policy, and macOS users routinely need several disjoint split tunnels up at once.
Lifting the policy alone is not enough. When two sessions run in one extension process, each
WireGuardAdapterlocates its tunnel device by taking the first utun control socket in the process, so the second tunnel attaches to the first tunnel's device and passes no traffic. That matches the report on the mailing list from July 2021 ("Running multiple tunnels simultaneously on macOS"), where two tunnels both reached Connected but only one passed traffic.The series, in order:
WireGuardKitC: include sys/types.h for BSD type definitions— build fix for the strict module verifier in current XcodeTunnelsManager: drop the restart intent when deactivation is requestedWireGuardKit: attach each adapter to its own utun device— the fix that makes concurrent tunnels actually pass trafficTunnelsManager: restrict single-tunnel activation policy to iOSPacketTunnelProvider: only exit on stop of last tunnel in the processmacOS: support multiple simultaneous tunnels in the menu bar UIAdd opt-in split DNS support— a per-tunnel toggle, so simultaneous tunnels do not fight over the system resolvermacOS: activate the app before showing a modal alert without a windowmacOS: don't crash opening the editor for an unreadable tunnelmacOS: reset peer endpoints after a network changeiOS behaviour is unchanged throughout, and every commit builds for both macOS and iOS on its own, so the series stays bisectable.
Tested on macOS 26 with Xcode 26: multiple concurrent tunnels passing traffic simultaneously on disjoint AllowedIPs, deactivating one tunnel leaving the others up, and DNS resolution verified with and without
DNS=entries as well as with split DNS enabled. The last commit was measured in daily use across network changes: handshake completion for the affected tunnel went from 2/130 to 83/129 once the peer stopped being pinned to an address that only existed on the previous network.This was sent to wireguard@lists.zx2c4.com on July 10 and July 25, which is the project's documented contribution channel, but the posts have not come out of the list's moderation queue. Opening it here as well so the work is findable by anyone hitting the same problem.