IPSec & IKEv2
IPSec is two protocols wearing one name: IKEv2 negotiates keys in userspace, the kernel moves the packets. That split explains most of what confuses people — why a tunnel can be 'up' with nothing passing, why ESP hides your payload but not who you are talking to, and why a dead peer blackholes traffic for minutes after the link dropped. This module walks strongSwan's IKEv2 state machine, the CHILD_SA it installs into the kernel, and the retransmission arithmetic that sets how long you wait.
What you'll explore
- 01
Four Messages, and What Each One Can Still See
IKEv2 establishes a working tunnel in two request/response exchanges — four messages. IKE_SA_INIT (exchange type 34) carries the proposals, the key-exchange payloads and the nonces; both sides can then derive keys. IKE_AUTH (35) is therefore already encrypted, which is why identities, certificates and the first CHILD_SA's traffic selectors are invisible to an observer while the SA proposals and Diffie-Hellman groups from the first exchange are not. That asymmetry is the practical one: a passive observer learns which crypto you negotiated and that two addresses are speaking IKE, and nothing about who they claim to be. Afterwards CREATE_CHILD_SA (36) adds or rekeys child SAs on the established IKE SA, and INFORMATIONAL (37) carries deletes, notifies and liveness checks — so 'an INFORMATIONAL arrived' is not itself good or bad news, and reading which notify it carried is the whole diagnosis. None of this is a linear function in strongSwan: ike_sa_t.process_message() validates the message against the current state and hands it to the task manager, which means an IKE failure surfaces as a state and a task rather than a line number.
- 02
The SA the Kernel Actually Enforces
The IKE SA carries no traffic. What moves packets is the CHILD_SA, and it exists twice: once as strongSwan's own object and once as state inside the kernel. child_sa_t.install_policies() is where the second one appears — it allocates a reqid if the CHILD_SA does not already have one, then installs the policies that tell the kernel which traffic to match and which SA to apply. This is why a tunnel can be genuinely 'up' in every status output and still pass nothing: the IKE SA is established, the CHILD_SA exists, and the traffic selectors do not cover the packets you are sending. Perfect Forward Secrecy lives here too, and it is not a global switch. determine_key_exchanges() reads the CHILD proposal for a KEY_EXCHANGE_METHOD transform; if there is none the code path is labelled, in source, 'no PFS' and the child keys are derived from the IKE SA's keying material alone. With the transform present, each CHILD_SA performs its own Diffie-Hellman, so compromising the IKE SA's keys later does not hand over the child keys — which is precisely the property the name describes, scoped to the child rather than to the connection.
- 03
ESP Hides the Payload, Not the Conversation
ESP and AH are both IP protocols, and only one of them encrypts. AH authenticates a packet and leaves every byte readable; ESP seals the payload with an AEAD. Read strongSwan's own ESP encryption and the boundary is exact: the additional authenticated data is the SPI and the sequence number — eight bytes — so those two fields are authenticated and travel in the clear, while the payload and the trailer that names the next header are encrypted. That is the honest version of 'ESP hides your traffic': an observer cannot read the payload, cannot see the inner protocol, and can still see the outer addresses, the SPI that identifies the SA, and the packet sizes and timing. Tunnel mode changes what 'outer' means — it wraps the original IP header inside the protected payload and writes a new one, so the inner addresses become part of what is encrypted and a gateway can protect traffic on behalf of hosts that know nothing about IPSec. Transport mode keeps the original header, which is why it only makes sense when the two endpoints are themselves the two hosts. ⚠️ A layer seam worth stating: strongSwan's own userland ESP (libipsec) implements ESP and has no AH at all. On Linux the data path is normally the kernel's XFRM stack, so an AH fact belongs to the kernel, not to this tree.
- 04
Why a Dead Peer Costs Minutes, Not Seconds
DPD is not a keepalive on a timer. ike_sa_t.send_dpd() checks whether an exchange is already in flight — if so it sends nothing, because a reply would prove liveness anyway — and otherwise compares the time since the last INBOUND traffic against the configured delay. Only if the peer has been silent for the full delay does it queue a request, and it then reschedules itself for delay minus the idle time already served. Two consequences follow, and the second is the one that costs outages. First, 'aggressive DPD floods a busy tunnel' has it backwards — but the reason is narrower than it first looks, and this is where reading the code beats reading the documentation. An exchange already in flight suppresses the check outright, and inbound IKE messages reset the comparison, so a tunnel busy with IKE is genuinely where DPD is quietest. DATA traffic is a different question: get_use_time() reads a counter that no data path writes to — the task managers set it from inbound IKE messages, it starts at SA creation, and after an IKE rekey charon deliberately rewinds it so a queued check is not sent — and it consults the CHILD_SAs' own use statistics only when a NAT has been detected — the source comment says 'only consider IPsec traffic if we use UDP-encapsulation and they take the same path'. So on a tunnel with no NAT, ESP traffic does not reset the clock at all: the tunnel can be saturated and still look idle to the IKE SA, and the check goes out every delay regardless of load. swanctl.opt states the broader behaviour — 'no IKE or ESP/AH packet has been received' — without that condition, and the code is what runs. Second, the delay is only when the question gets ASKED, not how long the answer takes. Once a request is queued it goes through the ordinary retransmission machinery, and for IKEv2 there is no separate DPD timeout: strongSwan's own documentation says dpd_timeout is IKEv1 only and has no effect on IKEv2, which uses the normal retransmission mechanism. The sequence is 4.0 × 1.8^try seconds, and charon gives up when tries is exceeded — with stock settings that is 4.0 + 7.2 + 12.96 + 23.33 + 41.99 + 75.58 ≈ 165 seconds after the delay expires. A 30-second DPD delay therefore means roughly 195 seconds of blackhole, not 30.
- 05
Detecting a NAT You Cannot See
Neither peer can observe a NAT directly; each only knows the addresses it believes are in use. So IKEv2 has both sides state that belief and compare. Each sends NAT_DETECTION_SOURCE_IP and NAT_DETECTION_DESTINATION_IP notifies whose payload is, in strongSwan's own comment, natd_hash = SHA1( spi_i | spi_r | address | port ). If the hash a peer computes over what it thinks your address is fails to match the hash you sent, something rewrote the packet in between — that mismatch IS the detection, and it needs no cooperation from the NAT. Once detected, ike_sa_t.float_ports() moves the conversation off UDP 500 to UDP 4500, and ESP is then encapsulated in UDP rather than sent as its own IP protocol. That second part is the operational point: ESP has no ports, so a NAT with several clients behind it has nothing to rewrite and no way to demultiplex the replies; wrapping ESP in UDP gives it a port number to translate. The cost is a permanent one — the UDP header rides on every data packet, and the mapping has to be kept alive, which is what charon's keep_alive interval does, defaulting to 20 seconds. Middleboxes that inspect UDP see what looks like an opaque stream on a fixed port, which is why NAT-T and deep-packet inspection are a recurring fight.
Validated against strongSwan (IKEv2 daemon, CHILD_SA installation, userland ESP). NOTE: on Linux the ESP data path is normally the KERNEL's XFRM stack, not strongSwan — charon installs the SA and steps out. strongswan-6.0.7 — src/libcharon/sa/ike_sa.c (IKE_SA state, DPD, NAT-T port float) and friends. Every simulated behavior cites a real source function, and a server-side correlation engine computes how each layer's state cascades into the next. See the methodology →
This module is part of zerohop Pro — it unlocks alongside the full kata library, progress tracking, and weak-area analysis.