IKEv2 Personal VPN: Child SA torn down after 120s idle (NEIKEv2ErrorDomain Code=15) with DisconnectOnIdle already NO

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

  1. 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?

  2. 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?

  3. Is there any supported way for an app to be notified of this teardown, given that fetchLastDisconnectError() returns nil for it?

  4. 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.

Answered by DTS Engineer in 904345022

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"

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"

Thanks, for the feedback.

I have attached the configuration profile we tested with (credentials and hostnames redacted). It is a bare IKEv2 payload: EAP-MSCHAPv2, no on-demand, no idle keys.

With this profile the tunnel stays up. We ran it three times and each session ran until we stopped it ourselves, with "Stop command received" as the stop reason and no idle teardown in the logs.

Our app, using NEVPNManager with NEVPNProtocolIKEv2 against the same server and account, does get torn down with NEIKEv2ErrorDomain Code=15 "IdleTimeout" after about 120 seconds of no traffic.

The differences we are aware of between the profile and what our app sets are:

  1. The app enables Connect On Demand (NEOnDemandRuleConnect with interfaceTypeMatch .any). The profile above has OnDemandEnabled = 0. The capture that showed the teardowns had onDemandEnabled = YES.

  2. The profile is installed as a managed payload, the app configuration is created through the Personal VPN API.

Two questions, if you are able:

  1. Is there anything in this profile that would not survive a port to the Personal VPN API? We noticed that the runtime configuration dump lists disconnectOnIdle and disconnectOnIdleTimeout inside the protocol dictionary, but there is no corresponding property on the public NEVPNProtocol, so we cannot set them from the app either way.

  2. Would you expect a Personal VPN with Connect On Demand enabled to be subject to an idle teardown that a profile without on-demand is not? That is currently our leading hypothesis for the difference.

I can supply a full sysdiagnose showing the teardown, along with the gateway side logs, privately if that is useful.

Hey hey, most people in your situation find that the problem reproduces with a profile, which gets me off the hook (-: The disparity between your profile and your code means I actually need to do some work!

It’s unlikely that the connect-on-demand stuff is a factor here, so I’m gonna focus on the disconnect-on-idle. You wrote:

We noticed that the runtime configuration dump lists disconnectOnIdle and disconnectOnIdleTimeout inside the protocol dictionary

Indeed. When you look in the system log you should see a log entry that includes these configuration properties. Do you see different values for the working (profile) case and the non-working (API) case?

I have lots of hints and tips about working with the system log in Your Friend the System Log. I usually do an analysis like this by triggering a log snapshot and then looking at the snapshot offline. In this case, you’ll want to focus on the com.apple.networkextension subsystem, which is where all the NE logging ends up.

Oh, and you’ll want to install the VPN (Network Extension) debug profile, available from our Bug Reporting > Profiles and Logs page.

I can supply a full sysdiagnose

Yeah, we might need that. But lemme first see if I can walk you through the process of how to find this stuff.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

IKEv2 Personal VPN: Child SA torn down after 120s idle (NEIKEv2ErrorDomain Code=15) with DisconnectOnIdle already NO
 
 
Q