We ship a consumer VPN app on iOS, iPadOS and tvOS using a Personal VPN configuration: NEVPNManager with NEVPNProtocolIKEv2, EAP-MSCHAPv2, no MDM profile installed. After exactly 120 seconds with no traffic, iOS destroys the Child SA and disconnects the tunnel. I would like to know whether that timer is configurable, and if not, what the intended mitigation is.
From a device sysdiagnose (iPhone, iOS 26.6, build 23G71): NEIPSecDBStatsUpdate: SA is idle for past 120 secs KernelSASession[1, IKEv2 Session Database] idle timeout SA Internal SAID = 2 SPI = C53320D1 Direction = Outbound ChildSA[1] state Connected -> Disconnected error Domain=NEIKEv2ErrorDomain Code=15 "IdleTimeout" <NEIKEv2Provider: Primary Tunnel>: stopping tunnel since Child disconnected nesessionmanager: plugin disconnected with reason "Tunnel was idle for too long"
This happened 12 times across a 9.2 hour overnight capture on one idle device. Median time before the tunnel re-established was 14m35s.
DISCONNECT-ON-IDLE IS NOT ENABLED
The same sysdiagnose shows the plugin's own configuration as: disconnectOnIdle = NO disconnectOnIdleTimeout = 0
These are the stock defaults. There is no disconnectOnIdle property on the public NEVPNProtocol, so a Personal VPN app cannot set them either way. The installed SA parameters contain no idle field at all, only "Lifetime Seconds = 1800", which is honoured correctly: the same capture shows 20 clean rekey cycles.
WHAT I HAVE RULED OUT
iOS logs a distinct stop reason for each of the following, and none of them occurred across 24 teardowns: "On Demand Disconnect rule matched", "Tunnel was terminated by the server", "Server is not responding", "Network changed, tunnel no longer viable", "Device went to sleep", "Stop command received". The only reason recorded was "Tunnel was idle for too long".
On the gateway (strongSwan), IKE rekey and reauth are disabled, uniqueids is never, and DPD is answered in roughly 200ms right up to the teardown. The gateway considers the tunnel healthy at the moment iOS tears it down.
DPD does not reset the timer, which makes sense: DPD is an INFORMATIONAL exchange on the IKE SA, whereas the log shows the timer measuring the OUTBOUND Child SA (SAID 2).
THE APP CANNOT SEE THIS HAPPEN
NEVPNConnection.fetchLastDisconnectError() returns nil for this teardown, because the error is in NEIKEv2ErrorDomain rather than NEVPNConnectionErrorDomain. The app has no supported way to detect that the tunnel dropped for this reason, or to distinguish it from a user-initiated disconnect. It is only visible in a sysdiagnose.
TRAFFIC IS NOT HELD DURING THE RECONNECT
With Connect On Demand enabled (NEOnDemandRuleConnect, interfaceTypeMatch .any), traffic after the teardown does not wait for the tunnel. On device wake following one of these drops:
01:32:43 device wakes 01:32:43 [C331 ... :443] path:satisfied @0.001s,
interface: en0[802.11], uses wifi 01:32:44 [C331 ... :443]
flow:finish_connect @0.623s (over en0) 01:32:47 tunnel status changed to connected
so flows complete over the physical interface for several seconds before the VPN re-establishes. On-demand triggered the reconnect but did not delay the traffic. For a VPN product this window is the part that concerns me most.
QUESTIONS
-
Is the 120 second Child SA idle timeout configurable for a Personal VPN using NEVPNProtocolIKEv2, from the app or from a configuration profile? If DisconnectOnIdle / DisconnectOnIdleTimer are meant to control it, why does the teardown occur when they are NO / 0?
-
If it is not configurable, is application-generated keepalive traffic the intended workaround? If so, how is that expected to work while iOS has the app suspended, which is exactly when a tunnel goes idle? Would server originated traffic that elicits a client response be a supported approach?
-
Is there any supported way for an app to be notified of this teardown, given that fetchLastDisconnectError() returns nil for it?
-
Is the behaviour in "traffic is not held during the reconnect" expected for NEOnDemandRuleConnect, or should matching flows be delayed until the tunnel is up? Is includeAllNetworks the only supported way to close that window?
Happy to supply the full sysdiagnose privately.
My general advice on this front is that you try to create a configuration profile that behaves as you’d like. If you can get that working then I can help you ‘port’ that to the Personal VPN API, or confirm that it’s not possible. OTOH, if you can’t get a configuration profile to work then you have a VPN configuration problem, and I’m not really able to help you with that. You might be able to make some progress with Apple Support resources, such as the Apple Support Community, run by Apple Support, and specifically the Business and Education topic areas.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"