The 2024 way to configure IKEv2 VPN server on Omnia

With Android 13 dropping support for PPtP and L2TP/IKEv1 VPNs, IKEv2 remained the only VPN type supported out of the box. As I like having the option to access my router from almost anywhere with just the knowledge of a password, I was looking for a way to get either EAP-MSCHAPv2 or PSK modes working. And good news, I got both! I did not care about certificate-base login, which is probably more secure, but less user-friendly.

(Un)fortunately, most clients require also a server or CA certificate to be installed for MSCHAPv2, so PSK remains the more universal one. However, it is not supported on Windows…

In this tutorial, I’ll show you how to get it running on Turris Omnia (hopefully all Turris routers would act the same). You can find tons of tutorials on the web for Linux, or even specifically for OpenWRT, however, most are kind of outdated. OpenWRT has been providing UCI configuration interface, so we’ll definitely want to use that. However, it didn’t support MSCHAPv2, so I’ve sent a pull request to include it, and this tutorial will make use of my fork (don’t worry, it’s just overwriting one file).


One confusion to get sorted out at the beginning: earlier, there was a OpenWRT script called ipsec that launched the VPN. It is part of a program called Strongswan. However, that one used directly the Strongswan config files and did not utilize UCI (a.k.a configuration in /etc/config). Later, there was some UCI support added. And lately, this ipsec script got deprecated and the modern way is swanctl. However, for some weird compatibility reasons, the config file is still called /etc/config/ipsec.

The goal

This tutorial will configure an Omnia that is reachable from the internet (it has a public IP) and which has a domain name that computers from the internet can resolve (you can either buy a domain or use e.g. no-ip.net dynamic DNS to get it for free). The domain is crucial. Some IKEv2 clients do not work with IP addresses only (iOS?).

Let’s suppose our router is visible from the internet as server1.ddns.net. We’ll configure two ways to connect to it. 1) PSK (shared key), 2) EAP-MSCHAPv2 (username + password, in this case user1 and password).

As there are also certificates involved, we’ll reuse the OpenVPN certificate authority (CA) already provided on the routers by the OpenVPN server Reforis package. If you don’t have it, enable it. Setting up OpenVPN in Reforis is a breeze!

The clients will be assigned dynamic addresses from range 192.168.21.2-192.168.21.254 which is different from the LAN network (we’ll use firewall zone called vpn). However, this range will be routed to LAN and WAN, so the connected clients will have access to both internet and LAN via the VPN (so called road-warrior setup).

This guide aims at configuring the server in such a way that it is accessible from all major client platforms with as few hassle as possible on the client side: Linux, Windows, Android, Apple.

Now that we know the goal, let’s dive in!

Install stuff

schnapps create "Before IKEv2 VPN config"

# schnapps savepoint 15  # If you're not at the router and you're scared you'll break the network; this will rollback to the previous state if you don't type `schnapps commit` in 15 minutes

opkg update
opkg install strongswan-full xfrm

# Update the swanctl init script to add MSCHAPv2 support and other fixes
wget https://raw.githubusercontent.com/peci1/packages/strongswan-mschapv2/net/strongswan/files/swanctl.init
cp /etc/init.d/swanctl{,-orig}
mv swanctl.init /etc/init.d/swanctl
chmod +x /etc/init.d/swanctl

# Install XFRM interface support for Luci (optional, but recommended; might require a reboot to stop showing "Unknown protocol").
wget https://raw.githubusercontent.com/openwrt/luci/openwrt-22.03/protocols/luci-proto-xfrm/htdocs/luci-static/resources/protocol/xfrm.js
mv xfrm.js /www/luci-static/resources/protocol/

# Generate server certificates. I decided to use the existing CA created for OpenVPN server managed by Reforis. If you don't use it, you also need to create a CA yourself.
# It is very important that CN= and --san= contain the exact domain under which your server is visible from the internet!
ipsec pki --gen --outform pem > /etc/swanctl/private/serverKey_server1.ddns.net.pem
ipsec pki --pub --in /etc/swanctl/private/serverKey_server1.ddns.net.pem | ipsec pki --issue --lifetime 3500 --cacert /etc/ssl/ca/openvpn/ca.crt --cakey /etc/ssl/ca/openvpn/ca.key --dn "C=CZ, O=Turris, CN=server1.ddns.net" --san="server1.ddns.net" --flag serverAuth --flag ikeIntermediate --outform pem > /etc/swanctl/x509/serverCert_server1.ddns.net.pem

Append the following to your /etc/config/network to create a virtual XFRM-type interface (don’t ask me what it is; just make sure ifid matches whatever you configure in /etc/config/ipsec):

config interface 'xfrm0'
        option ifid '357'
        option tunlink 'wan'
        option zone 'vpn'
        option proto 'xfrm'
        option multicast 'true'
        option mtu '1438'

config interface 'xfrm0_s'
        option proto 'static'
        option device '@xfrm0'
        list ipaddr '192.168.21.1/24'

Append the following firewall config to the end of `/etc/config/firewall’ (if you already have a different zone for VPN connections, you can reuse it and skip the forwards; just make sure to also change the name in the previously edited file):

config zone
        option name 'vpn'
        option conntrack '1'
        option input 'ACCEPT'
        option output 'ACCEPT'
        option forward 'ACCEPT'
        option masq '1'
        list network 'xfrm0_s'
        list network 'xfrm0'

config forwarding
        option src 'vpn'
        option dest 'wan'

config rule
        option target 'ACCEPT'
        option src 'lan'
        option dest 'vpn'

config rule
        option target 'ACCEPT'
        option src 'vpn'
        option dest 'lan'

config forwarding
        option dest 'vpn'
        option src 'vpn_turris'

config forwarding
        option dest 'vpn'
        option src 'lan'

config forwarding
        option dest 'lan'
        option src 'vpn'

config forwarding
        option dest 'vpn_turris'
        option src 'vpn'

config rule
        option target 'ACCEPT'
        option src 'wan'
        option _name 'ip_50_ESP'
        option proto '50'

config rule
        option target 'ACCEPT'
        option _name 'IP_51_AH'
        option src 'wan'
        option proto '51'

config rule
        option target 'ACCEPT'
        option _name 'IKE'
        option src 'wan'
        option proto 'udp'
        option dest_port '500'

config rule
        option target 'ACCEPT'
        option _name 'ipsec_NAT-T'
        option src 'wan'
        option proto 'udp'
        option dest_port '4500'

config rule
        option name 'AllowIPsec2WAN'
        list proto 'all'
        option src 'wan'
        option dest 'wan'
        option target 'ACCEPT'

Create a config for the VPN(s) in /etc/config/ipsec. Here’s an example setting up two different authentication methods: PSK (preshared key, easiest, but not available on Windows clients), and EAP-MSCHAPv2 (username+password, available everywhere).

config 'ipsec'
  list 'interface' 'wan'
  option 'zone' 'vpn'
  option debug 1  # increase up to 4 when debugging

config 'remote' 'eap'
  option 'enabled' '1'
  option 'gateway' 'any'
  option 'authentication_method' 'eap-mschapv2'
  option 'local_cert' 'serverCert_server1.ddns.net.pem'
  option 'rekeytime' '4h'
  option 'keyingtries' '3'
  option 'mobike' '1'
  option 'fragmentation' '1'
  list   'crypto_proposal' 'ike_proposal'
  list   'crypto_proposal' 'ike_proposal_ios'
  list   'crypto_proposal' 'ike_proposal_win'
  #list   'crypto_proposal' 'ike_proposal_win_insecure'
  #list   'crypto_proposal' 'ike_proposal_default'
  list   'tunnel' 'tun_clients'
  option local_identifier 'server1.ddns.net'
  list   pools 'pool'
  option send_cert always

 config 'remote' 'psk'
  option 'enabled' '1'
  option 'gateway' 'any'
  option 'authentication_method' 'psk'
  option 'pre_shared_key' 'password_that_has_to_be_20+_chars_long_for_ubuntu'
  option 'rekeytime' '4h'
  option 'keyingtries' '3'
  option 'mobike' '1'
  option 'fragmentation' '1'
  list   'crypto_proposal' 'ike_proposal'
  list   'crypto_proposal' 'ike_proposal_ios'
  #list   'crypto_proposal' 'ike_proposal_default'
  list   'tunnel' 'tun_clients'
  option local_identifier 'server1.ddns.net'
  option remote_identifier '%any'
  list   pools 'pool'
  option send_cert always

config 'crypto_proposal' 'ike_proposal'
  option 'encryption_algorithm' 'aes256gcm'
  option 'dh_group' 'modp3072'
  option prf_algorithm 'prfsha512'

config 'crypto_proposal' 'ike_proposal_ios'
  option 'encryption_algorithm' 'aes256gcm'
  option 'dh_group' 'modp2048'
  option prf_algorithm 'prfsha256'

config 'crypto_proposal' 'ike_proposal_win'
  option 'encryption_algorithm' 'aes256-sha256'
  option 'dh_group' 'modp2048'

config 'crypto_proposal' 'ike_proposal_win_insecure'
  option 'encryption_algorithm' 'aes256-sha1'
  option 'dh_group' 'modp1024'

config 'crypto_proposal' 'ike_proposal_default'
  option 'encryption_algorithm' 'default'

config pools 'pool'
  option addrs '192.168.21.2-192.168.21.254'
  list dns '192.168.21.1'

# we don't specify subnets because we're going to use XFRM-interfaced based routes instead
config 'tunnel' 'tun_clients'
  list   'remote_subnet' '0.0.0.0/0'
  list   'local_subnet' '0.0.0.0/0'
  option 'if_id' '357'
  option 'rekeytime' '1h'
  # other end is behind NAT or we'd use 'route' to initiate
  option 'startaction' 'none'
  option 'closeaction' 'none'
  list   'crypto_proposal' 'esp_proposal'
  list   'crypto_proposal' 'esp_proposal_ubuntu'
  #list   'crypto_proposal' 'esp_proposal_win_insecure'
  #list   'crypto_proposal' 'esp_proposal_default'
  option hw_offload 'auto'

config 'crypto_proposal' 'esp_proposal'
  option 'encryption_algorithm' 'aes256gcm'
  option 'dh_group' 'modp3072'

config 'crypto_proposal' 'esp_proposal_ubuntu'
  option 'encryption_algorithm' 'aes256-sha256'
  option 'dh_group' 'modp3072'

config 'crypto_proposal' 'esp_proposal_win_insecure'
  option 'encryption_algorithm' 'aes256-sha1'

config 'crypto_proposal' 'esp_proposal_default'
  option 'encryption_algorithm' 'default'

config mschapv2_secrets 'user1'
  option 'id' 'user1'
  option 'secret' 'password'

Now finish the setup:

/etc/init.d/network restart   # beware, this will disconnect you for a while!
ifconfig xfrm0  # should show something now
/etc/init.d/firewall reload
ip ro  # should show route 192.168.21.0/24 dev xfrm0 proto kernel scope link src 192.168.21.1 

/etc/init.d/swanctl enable
/etc/init.d/swanctl start

# Look at the generated swanctl config in /var/swanctl/swanctl.conf .
# If you have any problems, this is the format you'll find most help for.
# However, do not edit this file by hand. All changes have to be done to `/etc/config/ipsec`.

tail -fn100 /var/log/messages  # To see the connection log

schnapps create "Yaaay - IKEv2 working"
# schnapps commit

That’s it! Now you only need to set up the clients… Just be aware that it’s not possible to test the VPN connection from inside your network, so rather pull up your phone, connect data, disconnect wifi and try the config this way.

Client configuration

Ubuntu 20.04

It is possible to connect via NetworkManager, but you need to install a few programs.

sudo apt update
sudo apt install network-manager-strongswan libcharon-extra-plugins libstrongswan-extra-plugins strongswan-swanctl libcharon-standard-plugins
# reboot

After that, you can open network settings, click the + icon next to VPN and choose IPSEC/IKEv2 (strongswan). Gateway address is server1.ddns.net. You don’t need the certificate, but it is much better for security: copy the file /etc/swanctl/x509/serverCert_server1.ddns.net.pem to the Ubuntu machine and select it.

For PSK: Set Authentication to Pre-Shared Key. Username is user1 and password is the long password you specified for PSK (has to be 20+ characters, otherwise Ubuntu won’t allow you to save the config). Make sure to check Request an inner IP address. That’s It!

For EAP-MSCHAPv2: Set Authentication to EAP. The rest is the same as for PSK, except for the password.

Windows 10

Follow these tutorials in order:

  1. Storing a Windows Machine Certificate :: strongSwan Documentation to get the server certificate installed: /etc/ssl/ca/openvpn/ca.crt .
  2. Windows Client Configuration with Machine Certificates :: strongSwan Documentation but in step 3 choose “Username and password” authentication, and in the last step choose EAP and MSCHAPv2 (it will probably be selected)

Now you have two options:

  1. You increase the encryption level of the connection following tutorial How to configure cryptographic settings for IKEv2 VPN connections - Windows Security | Microsoft Learn (IKEv2 VPN Client)
  2. You decrease the security of your whole VPN by uncommenting the two lines in /etc/config/ipsec with ike_proposal_win_insecure.

There is no way to configure PSK authentication on Windows.

Android 13

For PSK authentication, you don’t even need to install the server certificate. So just add a new VPN connection, choose type IKEv2P/IPSEC PSK, then you need to fill something in the “IPSec identifier” (it shouldn’t matter, but can’t be empty), and then type the pre-shared key.

For EAP-MSCHAPv2 auth, you need to import the server certificate. Follow the tutorial chapter Import the server CA from: pfSense® software Configuration Recipes — Configuring IPsec IKEv2 Remote Access VPN Clients — Configuring IPsec IKEv2 Remote Access VPN Clients on Android | pfSense Documentation . The certificate to import is /etc/ssl/ca/openvpn/ca.crt from the router. Once this is done, you can continue, choose type IKEv2P/IPSEC MSCHAPv2, then fill in user in both IPSec identity and username, and fill password.

You can also install the “strongSwan VPN client” app from Play store. It only supports MSCHAPv2, but it will show you the full connection log, so it is great for figuring out why the connection doesn’t work.

iPhone

I haven’t succeeded configuring the VPN connection via the GUI. It should be somehow possible as some VPN providers have GUI-only guides, but who knows… Maybe it’s incompatible encryption, but I didn’t see any unmatched proposals in the logs… So, the workaround is to upload a full VPN profile to the phone where everything is already preconfigured.

I couldn’t get EAP-MSCHAPv2 working, although I think it might have been some dumb error. You can try harder. It seemed like the phone did not like the server cert, even though the CA was installed.

Anyways, to get any IKEv2 VPN working in iOS, you need to import the CA cert /etc/ssl/ca/openvpn/ca.crt. Upload the file to the phone somehow and then follow this guide: pfSense® software Configuration Recipes — Configuring IPsec IKEv2 Remote Access VPN Clients — Configuring IPsec IKEv2 Remote Access VPN Clients on iOS | pfSense Documentation . Just beware that once you “open” the cert, nothing will happen. You have to manually open Settings and there you’ll see the profile is waiting for your confirmation.

In a similar way, upload the VPN config file to the phone and follow the same procedure as for the CA. It will ask you for the pre-shared password once more when installing (not sure why). This should leave you with a configured VPN profile.

For PSK, the file is as follows (check it for comments telling you what to change except for the usual user1 and server1.ddns.net; I’ve marked these with $$text$$):

IKEv2-PSK.mobileconfig
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array>
		<dict>
			<key>IKEv2</key>
			<dict>
				<key>AuthName</key>
				<string>user1</string>
				<key>AuthenticationMethod</key>
				<string>SharedSecret</string>
				<key>ChildSecurityAssociationParameters</key>
				<dict>
					<key>DiffieHellmanGroup</key>
					<integer>14</integer>
					<key>EncryptionAlgorithm</key>
					<string>AES-256-GCM</string>
					<key>IntegrityAlgorithm</key>
					<string>SHA2-256</string>
					<key>LifeTimeInMinutes</key>
					<integer>1440</integer>
				</dict>
				<key>DeadPeerDetectionRate</key>
				<string>Medium</string>
				<key>DisableMOBIKE</key>
				<integer>0</integer>
				<key>DisableRedirect</key>
				<integer>0</integer>
				<key>EnableCertificateRevocationCheck</key>
				<integer>0</integer>
				<key>EnablePFS</key>
				<integer>0</integer>
				<key>ExtendedAuthEnabled</key>
				<integer>0</integer>
				<key>IKESecurityAssociationParameters</key>
				<dict>
					<key>DiffieHellmanGroup</key>
					<integer>14</integer>
					<key>EncryptionAlgorithm</key>
					<string>AES-256-GCM</string>
					<key>IntegrityAlgorithm</key>
					<string>SHA2-256</string>
					<key>LifeTimeInMinutes</key>
					<integer>1440</integer>
				</dict>
				<key>LocalIdentifier</key>
				<string>user1</string>
				<key>RemoteAddress</key>
				<string>server1.ddns.net</string>
				<key>RemoteIdentifier</key>
				<string>server1.ddns.net</string>
				<key>SharedSecret</key>
				<string>$$The shared secret password$$</string>
				<key>UseConfigurationAttributeInternalIPSubnet</key>
				<integer>0</integer>
			</dict>
			<key>PayloadDescription</key>
			<string>Configures VPN settings</string>
			<key>PayloadDisplayName</key>
			<string>VPN</string>
			<key>PayloadIdentifier</key>
			<string>com.apple.vpn.managed.FBFBDEF8-5B16-4863-91C1-7E2A68F848A4</string>
			<key>PayloadType</key>
			<string>com.apple.vpn.managed</string>
			<key>PayloadUUID</key>
			<string>425A1628-E99B-4547-966E-5B967CF1F5E4</string>
			<key>PayloadVersion</key>
			<real>1</real>
			<key>UserDefinedName</key>
			<string>$$Name displayed in the list of VPNs$$</string>
			<key>VPNType</key>
			<string>IKEv2</string>
		</dict>
	</array>
	<key>PayloadDisplayName</key>
	<string>$$Name displayed in the list of security profiles$$</string>
	<key>PayloadIdentifier</key>
	<string>C7918ABA-8DE8-40ED-A3AE-994CD40ACE23</string>
	<key>PayloadRemovalDisallowed</key>
	<false/>
	<key>PayloadType</key>
	<string>Configuration</string>
	<key>PayloadUUID</key>
	<string>9697F3C2-FF20-4981-A0C4-AA36BA78EEEB</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
</plist>

For completeness, here’s the EAP-MSCHAPv2 profile I’ve tried to use but didn’t succeed. It does not contain any secrets - the user will be asked for the username and password when installing the profile.

IKEv2-EAP.mobileconfig
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PayloadContent</key>
	<array>
		<dict>
			<key>IKEv2</key>
			<dict>
				<key>AuthenticationMethod</key>
				<string>Certificate</string>
				<key>ChildSecurityAssociationParameters</key>
				<dict>
					<key>DiffieHellmanGroup</key>
					<integer>14</integer>
					<key>EncryptionAlgorithm</key>
					<string>AES-256-GCM</string>
					<key>IntegrityAlgorithm</key>
					<string>SHA2-256</string>
					<key>LifeTimeInMinutes</key>
					<integer>1440</integer>
				</dict>
				<key>DeadPeerDetectionRate</key>
				<string>Medium</string>
				<key>DisableMOBIKE</key>
				<integer>0</integer>
				<key>DisableRedirect</key>
				<integer>0</integer>
				<key>EnableCertificateRevocationCheck</key>
				<integer>0</integer>
				<key>EnablePFS</key>
				<integer>0</integer>
				<key>ExtendedAuthEnabled</key>
				<integer>1</integer>
				<key>IKESecurityAssociationParameters</key>
				<dict>
					<key>DiffieHellmanGroup</key>
					<integer>14</integer>
					<key>EncryptionAlgorithm</key>
					<string>AES-256-GCM</string>
					<key>IntegrityAlgorithm</key>
					<string>SHA2-256</string>
					<key>LifeTimeInMinutes</key>
					<integer>1440</integer>
				</dict>
				<key>LocalIdentifier</key>
				<string></string>
				<key>RemoteAddress</key>
				<string>server1.ddns.net</string>
				<key>RemoteIdentifier</key>
				<string>server1.ddns.net</string>
				<key>UseConfigurationAttributeInternalIPSubnet</key>
				<integer>0</integer>
			</dict>
			<key>PayloadDescription</key>
			<string>Configures VPN settings</string>
			<key>PayloadDisplayName</key>
			<string>VPN</string>
			<key>PayloadIdentifier</key>
			<string>com.apple.vpn.managed.FBFBDEF8-5B16-4863-91C1-7E2A68F848A5</string>
			<key>PayloadType</key>
			<string>com.apple.vpn.managed</string>
			<key>PayloadUUID</key>
			<string>425A1628-E99B-4547-966E-5B967CF1F5E5</string>
			<key>PayloadVersion</key>
			<real>1</real>
			<key>UserDefinedName</key>
			<string>$$Name displayed in the list of VPNs$$</string>
			<key>VPNType</key>
			<string>IKEv2</string>
		</dict>
	</array>
	<key>PayloadDisplayName</key>
	<string>$$Name displayed in the list of security profiles$$</string>
	<key>PayloadIdentifier</key>
	<string>C7918ABA-8DE8-40ED-A3AE-994CD40ACE24</string>
	<key>PayloadRemovalDisallowed</key>
	<false/>
	<key>PayloadType</key>
	<string>Configuration</string>
	<key>PayloadUUID</key>
	<string>9697F3C2-FF20-4981-A0C4-AA36BA78EEEC</string>
	<key>PayloadVersion</key>
	<integer>1</integer>
</dict>
</plist>

Feel free to change any UUID-like value in the configs. They do not have any special meaning. But they have to be unique compared to other installed profiles.

Mac

I don’t have one around, help appreciated.

How to debug when something goes wrong

Start with increasing debug level in /etc/config/ipsec to 3 or 4. But then the log is too cluttered. I’ve crafted this line to show the most interesting parts:

tail -f /var/log/messages -n10000 | grep ipsec | grep -v -e JOB -e 'cert request' -e 'IKE. [ 0-9]\{4,\}: ' -e 'KNL. [ 0-9]\{4,\}:' | grep -P '(ipsec.......[^EN]|ENC. parsed|IKE_AUTH|retransmit|sending|received)'

For it to work, you need the better grep on the router (opkg install grep).

The interesting parts are around PROPOSALs - this is agreement on encryption. If the peers do not find a proposal that would satisfy both, that’s the end. You can try temporarily enabling the *proposal_default lines in /etc/config/ipsec and restarting swanctl to see whether there will be agreement with the broader encryption offer. However, it is best to comment them again once you figure out which encryption algorithms you need.

If you see some communication, authentication partly done and then the client stops communicating (you see no error, but connection doesn’t work), it is usually because the client figures out the server is not trustworthy. Check whether you have installed all the required certificates correctly.

On Ubuntu, make sure you checked "Request an inner IP address`. Without that, the connection will fail.

To view connection logs on Ubuntu, type sudo journalctl -xef.

Windows can show you some very scarce logs in eventvwr.

And, obviously, double-check the passwords! (I’ve spent a few hours going in circles before I figured I had a typo in some of the non-discoverable password fields).

Most useful links

https://docs.strongswan.org/docs/5.9/swanctl/swanctlConf.html

Known issues

When a second client connects, the first client remains connected, but cannot access anything via the VPN.

I think this has to be some kind of firewall/NAT/masquerade problem, but I haven’t figured it out yet. Help appreciated!

6 Likes

Connection test via the VPN from my 200/100 home site via my parent’s 70/20. The cap at 20 is given by the upload limitation of the VPN server’s connection, not by the ikev2 server itself.