VR on IPsec VPN Part 2 - Experimental Setup
VR on IPsec VPN Series:
- VR on IPsec VPN Part 1 - First Peek Into The Rabbit Hole
- VR on IPsec VPN Part 2 - Experimental Setup
1. Introduction
In Part 1, we went through why IKEv2 is such a juicy (and painful) target. Before we can start crafting packets or poking at the state machine, we need a lab that we fully control.
We will be building strongSwan from source instead of pulling it from the package manager. There are two good reasons for this:
-
Debug symbols – Compiling with
-g -O0makes life a lot easier when we start stepping through the daemon with gdb or cross-referencing functions in Ghidra later on. -
Session key extraction – The
save-keysplugin is not enabled in most distro builds. With it, strongSwan dumps the IKE and ESP session keys in a format Wireshark understands, so we can decrypt every captured message.
The lab consists of two Linux hosts on the same isolated network:
| Role | IP Address | IKE Identity | Notes |
|---|---|---|---|
| Client | 192.168.122.3 | road@lab | Requests a virtual IP from the server |
| Server | 192.168.122.4 | 192.168.122.4 | Protects 192.168.100.4/32 and hands out virtual IPs from 10.8.0.0/24 |

We will configure two flavours of the same road warrior connection: one authenticated with a Pre-Shared Key (PSK), and the other with digital signatures (certificates). This gives us captures of both IKE_AUTH variants to compare against in the later parts.
2. Building strongSwan from Source
The steps in this section apply to both the client and the server.
2.1 Installing Build Dependencies
First, get rid of any package-managed strongSwan so that it does not clash with our own build, then install the build dependencies.
# Remove any legacy / package-managed strongSwan
$ sudo apt remove --purge strongswan strongswan-starter strongswan-charon 2>/dev/null
$ sudo apt autoremove
# Build dependencies
$ sudo apt update
$ sudo apt install -y build-essential pkg-config libssl-dev libsystemd-dev
2.2 Downloading the Source
At the time of writing, the latest release is strongSwan 6.1.0.
$ cd /usr/local/src
$ sudo wget https://download.strongswan.org/strongswan-6.1.0.tar.gz
$ sudo tar -zxvf strongswan-6.1.0.tar.gz
$ cd strongswan-6.1.0
2.3 Compilation & Installation
Now, let’s configure the build. The important options here are:
--enable-systemdbuilds charon-systemd, a daemon that integrates with systemd and logs straight into the journal.--enable-save-keysbuilds the plugin that dumps session keys for Wireshark.CFLAGS="-g -O0"keeps debug symbols and disables optimizations, so that what we see in the debugger matches the source code.
$ sudo ./configure \
--prefix=/usr \
--sysconfdir=/etc \
--enable-systemd \
--with-systemdsystemunitdir=/etc/systemd/system \
--enable-save-keys \
CFLAGS="-g -O0"
$ sudo make -j$(nproc)
$ sudo make install
3. Configuring the Server
All connection configurations live in /etc/swanctl/conf.d/, and are loaded by swanctl --load-all when the service starts. To make our lives easier when testing, we shall configure the server to accept both PSK and certificate-based authentication methods.
3.1 Pre-Shared Key (PSK)
The server listens on 192.168.122.4 and accepts any initiator (id = %any) that knows the PSK. A few things worth pointing out:
proposalsrestricts the IKE SA to AES-CBC-128, SHA2-256 integrity, HMAC-SHA2-256 PRF and DH group 14 (modp2048). This is the exact proposal we will be dissecting in Part 3.dpd_delaymakes the server send an empty INFORMATIONAL message (heartbeat) every 30 seconds when the tunnel is idle.poolshands out virtual IPs to clients fromrw-pool.
connections {
rw-psk {
version = 2
local_addrs = 192.168.122.4
proposals = aes128-sha2_256-prfsha256-modp2048
rekey_time = 4h
dpd_delay = 30s
dpd_timeout = 120s
pools = rw-pool
local {
auth = psk
id = 192.168.122.4
}
remote {
auth = psk
id = %any
}
children {
net {
mode = tunnel
local_ts = 192.168.100.4/32
remote_ts = dynamic
esp_proposals = aes128-sha2_256
start_action = trap
dpd_action = clear
}
}
}
}
pools {
rw-pool {
addrs = 10.8.0.0/24
}
}
secrets {
ike-rw {
id = %any
secret = "lab-psk-change-me"
}
}
lab-psk-change-me secret is only meant for an isolated lab. Never reuse a weak, hardcoded PSK on a real deployment.3.2 Digital Signatures
For certificate-based authentication, we first need a Certificate Authority (CA). We can use strongSwan’s own pki tool to generate the CA, then issue a server certificate signed by it.
# Generate CA private key (ECDSA P-256)
$ pki --gen --type ecdsa --size 256 --outform pem > caKey.pem
# Self-sign the CA certificate (valid 10 years)
$ pki --self --ca --lifetime 3650 \
--in caKey.pem --type ecdsa \
--dn "CN=Lab CA" \
--outform pem > caCert.pem
# Generate server private key
$ pki --gen --type ecdsa --size 256 --outform pem > serverKey.pem
# Create a CSR for the server
$ pki --req --type ecdsa \
--in serverKey.pem \
--dn "CN=192.168.122.4" \
--outform pem > serverReq.pem
# Issue the server certificate (valid 2 years)
$ pki --issue --lifetime 730 \
--cacert caCert.pem --cakey caKey.pem \
--in serverReq.pem --type pkcs10 \
--san 192.168.122.4 \
--flag serverAuth \
--outform pem > serverCert.pem
Notice that the server certificate’s Subject Alternative Name (SAN) is set to 192.168.122.4. This must match the server’s IKE identity, otherwise the client will reject it during IKE_AUTH.
Next, install the certificates and private key into their respective swanctl directories:
$ sudo cp caCert.pem /etc/swanctl/x509ca/
$ sudo cp serverCert.pem /etc/swanctl/x509/
$ sudo cp serverKey.pem /etc/swanctl/private/
$ sudo chmod 600 /etc/swanctl/private/serverKey.pem
The connection configuration is nearly identical to the PSK one. The only differences are that auth is now set to pubkey, the server presents its own certificate, and clients must present a certificate signed by our CA.
connections {
rw-cert {
version = 2
local_addrs = 192.168.122.4
proposals = aes128-sha2_256-prfsha256-modp2048
rekey_time = 4h
dpd_delay = 30s
dpd_timeout = 120s
pools = rw-pool
local {
auth = pubkey
certs = serverCert.pem
id = 192.168.122.4
}
remote {
auth = pubkey
cacerts = caCert.pem
id = %any
}
children {
net {
mode = tunnel
local_ts = 192.168.100.4/32
remote_ts = dynamic
esp_proposals = aes128-sha2_256
start_action = trap
dpd_action = clear
}
}
}
}
pools {
rw-pool {
addrs = 10.8.0.0/24
}
}
3.3 Starting the Service
Enable and start the service:
$ sudo systemctl enable --now strongswan
$ sudo systemctl restart strongswan
Then, verify that both connections and the address pool are loaded:
$ sudo swanctl --list-conns
$ sudo swanctl --list-pools
4. Configuring the Client
4.1 Pre-Shared Key (PSK)
The client configuration mirrors the server’s. This time, we specify remote_addrs so that the client knows who to initiate to, and vips = 0.0.0.0 to request a virtual IP from the server’s pool. Setting start_action = start makes the client bring up the tunnel as soon as the configuration is loaded.
connections {
rw-psk {
version = 2
local_addrs = 192.168.122.3
remote_addrs = 192.168.122.4
proposals = aes128-sha2_256-prfsha256-modp2048
rekey_time = 4h
dpd_delay = 30s
dpd_timeout = 120s
vips = 0.0.0.0
local {
auth = psk
id = road@lab
}
remote {
auth = psk
id = 192.168.122.4
}
children {
net {
mode = tunnel
local_ts = dynamic
remote_ts = 192.168.100.4/32
esp_proposals = aes128-sha2_256
start_action = none
dpd_action = clear
}
}
}
}
secrets {
ike-rw {
id-1 = 192.168.122.4
secret = "lab-psk-change-me"
}
}
4.2 Digital Signatures
Generate the client certificate on the CA host (or wherever caKey.pem and caCert.pem reside), then copy the relevant files over to the client.
# Generate client private key (ECDSA P-256)
$ pki --gen --type ecdsa --size 256 --outform pem > clientKey.pem
# Create a CSR for the client
$ pki --req --type ecdsa \
--in clientKey.pem \
--dn "CN=road@lab" \
--outform pem > clientReq.pem
# Issue the client certificate (valid 2 years)
$ pki --issue --lifetime 730 \
--cacert caCert.pem --cakey caKey.pem \
--in clientReq.pem --type pkcs10 \
--san road@lab \
--flag clientAuth \
--outform pem > clientCert.pem
Install the certificates and private key on the client:
$ sudo cp caCert.pem /etc/swanctl/x509ca/
$ sudo cp clientCert.pem /etc/swanctl/x509/
$ sudo cp clientKey.pem /etc/swanctl/private/
$ sudo chmod 600 /etc/swanctl/private/clientKey.pem
connections {
rw-cert {
version = 2
local_addrs = 192.168.122.3
remote_addrs = 192.168.122.4
proposals = aes128-sha2_256-prfsha256-modp2048
rekey_time = 4h
dpd_delay = 30s
dpd_timeout = 120s
vips = 0.0.0.0
local {
auth = pubkey
certs = clientCert.pem
id = road@lab
}
remote {
auth = pubkey
cacerts = caCert.pem
id = 192.168.122.4
}
children {
net {
mode = tunnel
local_ts = dynamic
remote_ts = 192.168.100.4/32
esp_proposals = aes128-sha2_256
start_action = none
dpd_action = clear
}
}
}
}
4.3 Session Key Extraction
This is the part that makes the lab actually useful for research. Edit /etc/strongswan.conf on both hosts:
ike = 4undercharon-systemd.journalraises the IKE subsystem’s log level to the most verbose setting, which includes the raw session keys in the journal.- The
save-keysplugin writes the IKE SA keys toikev2_decryption_tableand the ESP SA keys toesp_sainside the Wireshark configuration directory, so Wireshark can decrypt the captured traffic on the fly.
charon-systemd {
journal {
default = 1
ike = 4
}
}
charon {
load_modular = yes
plugins {
include strongswan.d/charon/*.conf
save-keys {
load = yes
esp = yes
ike = yes
wireshark_keys = /home/kali/.config/wireshark
}
}
}
include strongswan.d/*.conf
wireshark_keys to the Wireshark configuration directory of whichever user runs Wireshark on your machine.5. Managing VPN Sessions
Ensure that the charon-systemd is enabled on startup and is running before we use swanctl to manage our VPN session.
$ sudo systemctl enable strongswan
$ sudo systemctl restart strongswan
To manage your VPN session, run the following commands:
# Start VPN session (Create IKE SA + Child SA)
## ------------- PSK ------------- ##
$ sudo swanctl --initiate --ike rw-psk --child net
## ------ Digital Signature ------ ##
$ sudo swanctl --initiate --ike rw-cert --child net
# Stop VPN session (Delete IKE SA)
## ------------- PSK ------------- ##
$ sudo swanctl --terminate --ike rw-psk
## ------ Digital Signature ------ ##
$ sudo swanctl --terminate --ike rw-cert
# Stop VPN session but preserve IKE SA (Delete Child SA only)
## ------------- PSK ------------- ##
$ sudo swanctl --terminate --ike rw-psk --child net
## ------ Digital Signature ------ ##
$ sudo swanctl --terminate --ike rw-cert --child net
7. Conclusion
We now have a strongSwan server and client built with debug symbols, two road warrior connections using PSK and certificate authentication, and session keys being dumped straight into Wireshark. With this setup, every IKEv2 message going across the wire can be decrypted and inspected.
In the next part, we will use this lab to break down the different IKEv2 message types, and compute the session keys ourselves to understand exactly what goes into each message.
8. References
- strongSwan Downloads
- strongSwan swanctl.conf documentation
- strongSwan pki documentation
- strongSwan save-keys plugin
- strongSwan Logging documentation
- RFC 7296 – Internet Key Exchange Protocol Version 2 (IKEv2)
9. Resources
VR on IPsec VPN Series:
- VR on IPsec VPN Part 1 - First Peek Into The Rabbit Hole
- VR on IPsec VPN Part 2 - Experimental Setup