#21355·tailscale

iOS client tears down the tunnel during large uploads; macOS client on the same network and same upload does not

Author: InhumCreated Sep 17, 2026Updated Sep 17, 2026
LabelsOS-iosbug

What is the issue?

On the same residential Wi-Fi, in the same tailnet, performing the same operation — sending a large photo or video from a messaging app whose traffic is routed through an app connector:

  • macOS client: the upload completes.
  • iOS client: the tunnel stops passing traffic and is then stopped by the system.

On iOS:

  • The failure happens while a large upload is in progress.
  • Small traffic continues to pass at the same time: text messages, ICMP between tailnet peers.
  • Retrying the upload usually succeeds, but noticeably slower.
  • Recovery usually requires a network change on the device (e.g. toggling airplane mode).
  • Reproduces on residential Wi-Fi and on a mobile carrier network.
  • Reproduces with an exit node selected and with exit node set to None.

What is expected?

The iOS client completes large uploads through the tunnel, as the macOS client does on the same network, and the Network Extension is not stopped.

Steps to reproduce

  1. iOS device and macOS device on the same residential Wi-Fi, same tailnet, same user; tailnet with two app connectors (configuration size below).
  2. Exit node: None (also reproduces with an exit node selected).
  3. On macOS, send a large video from the messaging app's desktop client. It completes.
  4. On iOS, send a large photo or video from the same messaging app. The upload stalls and the client logs stopTunnel with NEProviderStopReason.rawValue = 17.

Are there any recent changes that introduced the issue?

No known regression date.

OS / versions

iOS client macOS client
Device iPhone 13 MacBook Air (M1)
OS iOS 18.7.8 (22H352) macOS
Tailscale 1.102.3 when the log below was captured; now 1.102.4 1.102.4

App connectors: Linux, Tailscale 1.102.3.

Tailnet configuration (orders of magnitude)

  • 2 app connectors; 36 app connector blocks in the policy, ~230 domains, ~210 static CIDRs.
  • App connectors advertise ~2900 routes in total.
  • Routes installed on the macOS client (netstat -rn, Tailscale utun interface): ~1550 IPv4 / ~44 IPv6 in one measurement, ~1730 IPv4 / ~9 IPv6 in a later one.
  • Difference between exit node = None and an exit node selected, on the macOS client: 6 routes (~1551 vs ~1557 IPv4).

Collected data

iOS client log around the failure. Local timestamps; addresses and node identifiers replaced with placeholders.

09:22:30  control: PollNetMap: read tcp [<client-ipv6>]:57354->[<control-plane-ipv6>]:443:
          read: connection reset by peer
09:22:30  control: mapRoutine: backoff: 8 msec
09:22:30  control: controlhttp: forcing port 443 dial due to re...
09:22:31  control: controlhttp: succeeded dialing "controlplane.t..."
09:22:31  control: netmap: got new dial plan from control
09:22:31  control: new network map (periodic)
09:22:31  netmap diff: (none)
09:22:33  gui: [ipnBus] - Stopping IPN Watch Session  (x2)
09:22:34  wg: [<peer-1>] - Sending handshake initiation / Received handshake response
09:22:38  wg: [<peer-2>] - Received handshake initiation / Sending handshake response
09:22:38  Accept: ICMPv4{<peer-2-ip>:0 > <this-device-ip>:0} 84
09:22:38  Accept: ICMPv4{<this-device-ip>:0 > <peer-2-ip>:0} 84
09:22:39  open-conn-track: timeout opening (TCP <this-device-ip>:57365 => ...)
09:22:41  swiftNetMon: Path evaluated - unchanged
09:22:43  stopTunnel: "unhandled reason, NEProviderStopReason.rawValue = 17"
09:22:43  Persistency maintained for stop reason unhandled reason
09:22:43  LinkChange: minor
09:22:43  swiftNetMon: Path evaluated - unchanged
09:22:43  network update from Swift: connected=true, expensive=false
09:22:43  wg: Failed to write packets to TUN device: write /dev/tun: destination address required
09:22:44  wg: Failed to write packets to TUN device: write /dev/tun: bad file descriptor
09:22:45  wg: Failed to write packets to TUN device: write /dev/tun: bad file descriptor
09:22:46  wg: Failed to write packets to TUN device: write /dev/tun: bad file descriptor
09:22:48  wg: [<peer-2>] - Receiving keepalive packet
  • Sequence: control connection reset → successful re-dial, netmap diff empty → WireGuard handshakes → ICMP through the tailnet accepted in both directions → open-conn-track timeout → stopTunnel.
  • 17 is NEProviderStopReasonInternalError (per NEProvider.h in the iOS SDK; available since iOS 18.1 / macOS 15.1). The client logs it as an unhandled reason.
  • swiftNetMon reports the path unchanged before and after the stop.
  • Extension memory in this log: 32.0–32.5 MB throughout.
  • A full bug report can be provided on request.

macOS client, same residential Wi-Fi:

  • Tunnel interface MTU: 1280. Largest DF ICMP packet through the tunnel to an app connector's tailnet address: 1280 bytes (1281 fails).
  • Physical path MTU: DF ping (ping -D) with 1300, 1400 and 1500-byte packets succeeds to the app connector's public address and to two unrelated public hosts; 1500 bytes passes to all three.
  • Peer connection: both app connectors reached via DERP relay; tailscale ping reports direct connection not established.
  • tailscale netcheck: UDP: true, IPv4: yes, IPv6: no, but OS has support, MappingVariesByDestIP: false, CaptivePortal: false, nearest DERP ~65 ms.
  • Control upload: a ~14 MB video sent from the messaging app's desktop client, routed via the same app connector. Sampled every ~2 s: ~16.7 MB of tunnel tx to the connector in ~12 s, no ICMP loss to the connector, backend state Running throughout. The upload completed.

iOS client, not collected: tunnel interface MTU and peer connection type (direct vs relay).

  • iOS client tunnel MTU: not visible in client logs; the shared Go default (net/tstun) is 1280, the value applied by the iOS app was not verified.

What was ruled out (measurements)

  • Network Extension crash: no IPNExtension-*.ips entries in device analytics since 2026-08-01, while failures occur daily.
  • Jetsam kill: 14 JetsamEvent-* files over ~9.5 months (about one per 20 days). No jetsam event at the time of the failure shown above. In the two parsed events (2026-09-06, 2026-09-14) the killed process was SpringBoard (per-process-limit); nesessionmanager (7–10 MB) and IPNExtension (21–32 MB) were alive.
  • Route count difference between modes: exit node vs None differs by 6 routes out of ~1551; both modes reproduce.
  • Physical path MTU on the shared residential Wi-Fi: 1500 bytes (measured from the macOS client).
  • Other VPN configuration on iOS: VPN On Demand disabled in the client; no other VPN profile on the device.