IPsec IKEv2 MSCHAPv2 VPN server
For modern clients, IPsec IKEv2 MSCHAPv2 is now the preferred VPN solution. It is supported by Windows since Windows 7, Android since 11, macOS since 10.11, iOS since 9. Both full tunnel and split tunnel configurations are possible (Split tunnel may be require additional configuration on the client)
Introduction
IPsec IKEv2 MSCHAPv2 is VPN protocol commonly supported now. This guide will not cover setting up DHCP or RADIUS. PKI will also not be covered, but the app-crypt/easy-rsa package can quickly create a PKI suitable for use for a VPN server. Its also possible to create server certificate signed by a real CA like Let's_Encrypt. IPv6 is not covered, even though its a first-class citizen in IPsec IKEv2 MSCHAPv2 - the reason being is for dynamic IP strongSwan would need to support a prefix delegation (PD) client, which it currently does not. In addition, Windows client is different with respect to ipv6 to all other clients.
Client configuration for common client will be covered.
Assumptions and example settings
- Domain is example.com
- Server name is vpn.example.com
- CA file is called ca.crt
- Server cert is vpn.example.com.crt
- Server key is vpn.example.com.key
- IP being server to the clients is 172.21.119.0/24, except 172.21.119.1 (which some configuration need for the server).
- Linux Kernel at least 5.4
- USing current stable version of net-vpn/strongSwan in Portage
Before emerging strongSwan
The eap USE Flag needs to be set on net-vpn/strongSwan
Create missing directories
Some of the strongSwan directories are not create by either strongSwan itself or the ebuild currently, so those needs to be created:
root #
( umask 007 ;\
mkdir /etc/swanctl/{bliss,conf.d,ecsda,pkcs12,pkcs8,private,pubkey,rsa};\
mkdir /etc/swanctl/x509{,aa,ac,ca,crl,ocsp};\
mkdir /etc/ipsec.d;\
mkdir /etc/ipsec.d/{private,certs,crls,ocspcerts,aacerts,acerts,reqs}; )
Copy certificates and keys to the correct place
root #
cp ca.crt /etc/swanctl/conf.d/x590ca
root #
cp server.example.com.crt /etc/swanctl/conf.d/x509
root #
cp server.example.com.key /etc/swanctl/conf.d/private
Server configuration 1:Base configuration
Each server configuration will have its own section, this is the one the be used as a template for the others. Note that configuration CAN be mixed and matched, and is encouraged.
Configuration File
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=ike2-roadwarriors
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/0
}
}
}
}
pools {
ike2-roadwarriors {
addrs = 172.21.119.2-172.21.119.254
}
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
Here, an IPsec responder is defined that serves IP from 172.21.119.0/24 block, and a single user named "avatar" with the password "unobtainium". Multiple pools and usernames can be defined. For a guide to the config file format, see swanctl.conf
There are 2 extra lines to required to deal with buggy clients. The proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default is for Windows, which will not negotiate a DH higher than modp1024 without a registry hack (described later). The send_cert=always is for the native Android client, which doesn't ask for it like other clients but needs it.
Server configuration 2: Policy based, full-tunnel VPN
Configuration file
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=ike2-roadwarriors
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/0
}
}
}
}
pools {
ike2-roadwarriors {
addrs = 172.21.119.2-172.21.119.254
dns = 192.158.50.1
}
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
The configuration is pretty much the same as above. If there's an internal DNS server, it can be specified in the pools section, this is required for "split-horizon" DNS setup.
VPN Server is the default gateway
No extra action is required. It Just Works.
VPN Server is the not default gateway
Extra steps are required. The VPN server will be able to send out packets, but the other clients on the network won't know how to get them back to VPN server, resulting in one-way traffic. There are several solution, but the easiest to the NAT the clients.
NAT with iptables
Replace eth0 with the real outgoing interface
root #
iptables -t nat -A POSTROUTING -s 172.21.119.0/24 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
root #
iptables -t nat -A POSTROUTING -s 172.21.119.0/24 -o eth0 -j MASQUERADE
NAT with nftables
Replace eth0 with the real outgoing interface
#!/sbin/nft -f
table ip ipsec-nat {
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 172.21.119.0/24 oifname "eth0" rt ipsec exists
ip saddr 172.21.119.0/24 oifname "eth0" masquerade
}
}
Server configuration 3: XFRM Route based, full-tunnel VPN
Configuration File
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=ike2-roadwarriors
if_id_in=500
if_id_out=500
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/0
}
}
}
}
pools {
ike2-roadwarriors {
addrs = 172.21.119.2-172.21.119.254
dns = 192.158.50.1
}
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
The new additions are the if_id_in=500 and if_id_out=500. The value 500 is arbitrary. Any nonzero u32 is valid.
Creating interface
Note the interface may (and should) be create before strongSwan starts. Note that, as of Feb 2022, neither NetworkManager nor Netifrc (bug #443480) support creation of xfrm interfaces, while systemd-networkd does.
To create the interface (replace eth0 with the real outgoing interface):
root #
ip link add xfrm0 type xfrm dev eth0 if_id 500
root #
ip addr add 172.21.119.1/24 dev xfrm0
root #
ip link set xfrm0 up
xfrm0 is an arbitrary name. The if_id must match the value in /etc/swanctl/conf.d/vpn.example.com.conf. Note that 172.21.119.1 was intentionally left out of the client IP pool so the server could claim it. If the server has as static allocation of ipv6 addresses, the interface may be assigned an ipv6 address too.
VPN Server is the default gateway
No extra action is required. It Just Works.
VPN Server is the not default gateway
Extra steps are required. The VPN server will be able to send out packets, but the other clients on the network won't know how to get them back to VPN server, resulting in one-way traffic. There are several solution, but the easiest to the NAT the clients.
NAT with iptables
Replace eth0 with the real outgoing interface (NOT the xfrm interface)
root #
iptables -t nat -A POSTROUTING -s 172.21.119.0/24 -o eth0 -j MASQUERADE
NAT with nftables
Replace eth0 with the real outgoing interface (NOT the xfrm interface)
#!/sbin/nft -f
table ip ipsec-nat {
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 172.21.119.0/24 oifname "eth0" masquerade
}
}
Server configuration 4: vti Route based, full-tunnel VPN
Charon configuration
Edit /etc/strongswan.d/charon.conf and change install_routes to no
Configuration file
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=dhcp
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/0
mark_in=0x0001
mark_out=0x0001
}
}
}
}
pools {
ike2-roadwarriors {
addrs = 172.21.119.2-172.21.119.254
}
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
Interface configuration
root #
ip tunnel add vti0 loocal 192.168.50.68 remote 0.0.0.0 mode vti key 0x0001
root #
sysctl -w net.ipv4.conf.vti0.disable_policy=1
root #
ip addr add 172.21.119.1/24 dev vti0
root #
ip link set vti0 up
Substitute 192.168.50.68 with the real outgoing interface.the key value must match the mark_in and mark_out value in the config file.
Note that 172.21.119.1 was intentionally left out of the client IP pool so the server could claim it. For IPv6 users, a separate vti6 interface will be required.
VPN Server is the default gateway
No extra action is required. It Just Works.
VPN Server is the not default gateway
Extra steps are required. The VPN server will be able to send out packets, but the other clients on the network won't know how to get them back to VPN server, resulting in one-way traffic. There are several solution, but the easiest to the NAT the clients.
NAT with iptables
Replace eth0 with the real outgoing interface (NOT the vti interface)
root #
iptables -t nat -A POSTROUTING -s 172.21.119.0/24 -o eth0 -j MASQUERADE
NAT with nftables
Replace eth0 with the real outgoing interface (NOT the vti interface)
#!/sbin/nft -f
table ip ipsec-nat {
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 172.21.119.0/24 oifname "eth0" masquerade
}
}
Server configuration 5: Policy based, split-tunnel VPN
Configuration File
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=ike2-roadwarriors
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=192.168.50.0/24
}
}
}
}
pools {
ike2-roadwarriors {
addrs = 172.21.119.2-172.21.119.254
}
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
The change this time is in the local_ts parameters. This only allows traffic going to 192.168.50.0/24 (an internal network, replace with the real internal network). It up to the client as to what to do with other traffic. Most client (including strongSwan itself) will not send such traffic over the tunnel and let it go over through the appropriate interface.. Windows is the exception, and by default, attempts to send all traffic over the tunnel, unless told not to.
If using split-horizon DNS, add a DNS server to the pool (like the full-tunnel case), but additional care is needed, that the DNS server is in the range of local_ts and that it can resolve all addresses including those outside the work, otherwise the client may lose the ability to resolve non-internal sites.
If the a route-based VPN server is desired, see the section about about route-based VPN. IF the server is not the default gateway, see the sections about setting up NAT.
Server configuration 6: DHCP addressing, policy-based full-tunnel VPN
net-vpn/strongswan needs to dhcp and farp flags configured. This plugin only works with DHCPv4.
Plugin configuration
IF the VPN server is not the DHCP server, no additional configuration is required. If the VPN server is the DHCP server, configuration is required if the DHCP daemon does not listen on localhost. See dhcp plugin for the configuration details.
To configure the plugin, edit /etc/strongswan/charon.d/dhcp.
Server configuration
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=dhcp
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-mschapv2
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/24
}
}
}
}
pools {
}
secrets {
eap-1 {
id=avatar
secret=unobtainium
}
}
authorities {
}
The change this time is pools=dhcp. This will serve client IP address from the DHCP server. For a split tunnel server, set local_ts to the internal network
VPN Server is the default gateway
No extra action is required. It Just Works.
VPN Server is the not default gateway
NAT with iptables
Replace eth0 with the real outgoing interface. This is mostly the same, with one addition: The server itself should not NAT itself. Replace 192.168.50.68 with the real outgoing interface IP and 192.168.50.0 with the IP pool issued by the DHCP server.
root #
iptables -t nat -A POSTROUTING -s 192.168.50.68 -o eth0 -j ACCEPT
root #
iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o eth0 -m policy --dir out --pol ipsec -j ACCEPT
root #
iptables -t nat -A POSTROUTING -s 192.168.50.0/24 -o eth0 -j MASQUERADE
NAT with nftables
Replace eth0 with the real outgoing interface. This is mostly the same, with one addition: The server itself should not NAT itself. Replace 192.168.50.68 with the real outgoing interface IP and 192.168.50.0 with the IP pool issued by the DHCP server.
#!/sbin/nft -f
table ip ipsec-nat {
chain POSTROUTING {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 192.168.50.68 oifname "eno1"
ip saddr 172.21.119.0/24 oifname "eth0" rt ipsec exists
ip saddr 172.21.119.0/24 oifname "eth0" masquerade
}
}
Server configuration 7: RADIUS authentication/pools, policy based full tunnel
Plugin configuration
To configure the plugin, edit /etc/strongswan/charon.d/eap-radius.conf. See EAP-Radius for details.
Configuration File
connections {
linuxvpn-ikev2-mschap {
version=2
send_cert=always
proposals=aes128-aes192-aes256-sha1-sha256-sha384-modp1024,default
pools=ike2-roadwarriors
local-1 {
auth=pubkey
certs=vpn.example.com.crt
id = @vpn.example.com
}
remote-1 {
auth=eap-radius
eap_id=%any
}
children {
linuxvpn-ikev2-mschap {
local_ts=0.0.0.0/0
}
}
}
}
pools {
}
secrets {
}
authorities {
}
Here, auth on remote-1 changes to eap-radius. The pool and secrets section is empty, as the values will be returned by the RADIUS server. For a split tunnel, adjust local_ts. For a route-based VPN, see the above section. If the VPN server is not the default gateway, see the above section on NAT.
Note that many clients perfer EAP-PEAPv0/MSCHAPv2 over EAP-MSCHAPv2, so clients will likely start using the former if its available. EAP-PEAPv0/MSCHAPv2 requires some special OIDs on the RADIUS server certificate.
Client configuration notes
Windows
Enabling split tunneling
BY default, Windows connects via full tunnel mode even if the server local traffic selector are restricted, dropping extra traffic. To get Windows to split tunnel, there are 2 options
Enable split tunneling via the GUI
Go to "Change adapter options" to show the adapters. Right-click the VPN connection, choose Properties, then Networking, then Internet Protocol Version 4 (TCP/IPv4), then Properties, then Advanced, then uncheck "Use default gateway on remote network".
Via PowerShell
Set-VPNconnection -name vpn.example.com -SplitTunneling $true
Substitute vpn.example.com with the given VPN connection name
Making a more secure proposal
Windows won't offer and IKE proposal better then modp1024, which strongSwan does not include in the default proposal. It is however, possible to windows to aes256-sha1-modp2048 through a registry addition. Create the key HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Rasman\Parameters\NegotiateDH2048_AES256 as a DWORD. Set it to 1 to allow windows to accept a proposal of aes256-sha1-modp2048. Set it to 2 to force windows disallow lesser proposals.
MacOS
The "Remote name" must be the subjectAltName or CN name of the VPN server (usually, it is DNS names). The "Local Name" may remain blank.
Android
The native android client ha a few bugs. First, even though the IPSec identifier says (unused), it must actually be populated, but the value is unimportant. Second, even though the IPSec CA certificate is "optional", it isn't if using a CA (like a internal one) Android does not know about - Android will fail to connect without it.
Android 11
The native client for Android 11 does not work. See strongSwan issue # 3673. This is an Android bug. not a strongSwan one. Until (if) Google fixes it, use the strongSwan client instead.
iOS
If the VPN server certificate is not signed by an authority known by iOS, the certificate must be downloaded (either via Safari or e-mail attachment) and installed as "profile" in Settings. Note a raw .crt file is needed, iOS won't install the certificate from a PKCS#12 bundle.