jkipsec
Userspace IKEv2/IPsec responder built to terminate iOS VPN tunnels and hand
the inner IP traffic to your own code - typically jktcp
for a complete userspace TCP/IP stack.
iOS device Internet jkipsec
──────────── ──────── ──────────
UDP 500
IKE_SA_INIT ──────────────────────────────────► ┐
│ Server task
IKE_AUTH(SK) ─────────────────────────────────► │ (no locks,
│ crossfire
ESP packets ─────────────────────────────────► │ channels)
│
▼
decrypted IP packets
│
▼
jktcp::Adapter
│
▼
your application code
What this is
A small Rust library that speaks just enough IKEv2 + ESP to let an iOS device
build an IPsec VPN tunnel to a process you control, with the inner IP packets
exposed as an AsyncRead + AsyncWrite stream you can plug into a userspace
TCP stack.
It exists because:
- iOS' WireGuard implementation creates a
utuninterface whose policies block some inbound services (notably lockdown on TCP/62078). iOS' built-in IKEv2 client uses the same OS path but with different policy hooks, and can reach those services. (edit: ipsec is now blocked too) - Implementing IKEv2/IPsec in userspace lets you run as a non-root user and multiplex multiple devices without touching the kernel.
The companion crate jktcp gives you a full
userspace TCP stack on top of the raw IP frames jkipsec produces.
What this is not
- Not a production VPN gateway. Designed for terminating one or a few trusted iOS devices to talk to specific services on the host.
- Not a general IPsec implementation. Negotiates exactly two IKE/ESP suites, both with DH group 19 (P-256). No MODP groups, no AH, no transport mode, no IPv6 inner support, no rekeying, no DPD, no MOBIKE, no fragmentation, no IKE_INTERMEDIATE / RFC 9370 hybrid PQ.
- Not security-audited. The crypto primitives are RustCrypto, but the protocol code is hand-written and lightly tested. Don't use it where a motivated attacker on the network is part of your threat model.
- Not a drop-in
strongSwan/Libreswanreplacement. Tested against iPhone 5, iPhone 8, and modern iPhones - that's it. Other clients (Windows, Android, strongSwan-as-initiator) are likely to fall outside the narrow set of behaviors we accept.
Negotiated suites
Both suites use DH group 19 (NIST P-256) for IKE_SA_INIT and PRF HMAC-SHA-256. The selector tries them in this preference order and picks the first one the peer offers:
| # | Cipher | Integrity | Notes |
|---|---|---|---|
| 1 | AES-GCM-16-256 (AEAD) | (built into AEAD) | Modern iOS default |
| 2 | AES-CBC-256 | HMAC-SHA-256-128 | Older iOS (iPhone 5–8) only ships this |
Authentication is PSK only (RFC 7296 §2.15 shared key MIC). The "PSK" is
never stored - only the 32-byte derived auth key, which the user's callback
returns or verifies via AuthChallenge::approve_with.
Quick start
Cargo.toml:
[]
= "0.1"
= "0.1"
= { = "1", = ["full"] }
use ;
use ;
use derive_auth_key;
use ;
async
A more complete runnable demo lives in examples/probe.rs:
JKIPSEC_PUBLIC_IP=1.2.3.4 \
JKIPSEC_PSK=secret \
JKIPSEC_PROBE_PORTS=12345 \
iOS profile
Configure on the device (Settings -> VPN -> Add VPN Configuration):
- Type: IKEv2
- Server: your server's public IP
- Remote ID: must match
JkispecConfig.identity - Local ID: any string - this becomes the
identityinAuthChallenge - User Authentication: None
- Use Certificate: off
- Secret: set to the raw PSK whose
derive_auth_key(...)you stored
For older iOS, the same flow works; jkipsec falls back to AES-CBC + HMAC.
Architecture notes
- Single server task owns all IKE/ESP state. UDP readers, UDP writers,
per-session outbound ESP encryptors, and the application code all
communicate through lockless
crossfirechannels - noArc<Mutex<...>>on hot paths. - Per-session outbound ESP runs in its own task, owning its keys and
using
UdpSocket::send_todirectly. ESP fast-path therefore avoids any channel hop on send. EspTunnelwrapsMAsyncRx+MTxto satisfy jktcp'sSyncbound. The singlestd::sync::Mutexit holds is uncontended (onlypoll_readtouches it) and exists solely to bridge the trait bound.- NAT-T: detected automatically; iOS will float to UDP/4500 with the 4-byte non-ESP marker. We bind both ports.
- Split tunnel: TSr in IKE_AUTH is narrowed to a
/32forgateway_ip, so iOS only routes traffic destined for that single address through the tunnel - your gateway code is the only thing the device sees.
Testing
22 unit tests cover the IKE parser, crypto primitives, ESP round-trips for both suites, and an end-to-end IKE_SA_INIT round-trip against a captured iOS packet.
License
MIT - see LICENSE.