Strongswan IKEv2 split/full tunnel VPN on Alpine Linux VM on Synology Diskstation
Last Updated Mar 14, 2019
- Background
- The Proposal
- Assumptions
- Creating Alpine Linux VM on Synology Diskstation 6
- Preparing configuration files for IPsec
- Setting up PKI and generating certificates
- Network infrastructure configuration
- Configuring client devices
- Debugging
- Closing notes
- References
- History
Background
I needed to have a VPN server at home that I would connect to from anywhere to enjoy:
- Security of encrypted tunnel
- Access to Web content filter deployed at home
- Access to machines in my home network as needed
Requirements
Nothing unreasonable here:
- Configuration on a client should be simple. Ideally – one-click.
- Connection should be automatic, resilient to network interruptions, and should support always-on.
- Routing between LAN and VPN devices shall work
- DNS resolution shall work
- Certificate-based authentication desirable. EAP will also do.
- Support for full tunnel and split tunnel, including split-DNS mode, and quick switching between the two.
- iOS, macOS support are a must, Windows 10 support is desirable
What’s available
- Ubiquiti USG
- supports remote access VPN via L2TP and OpenVPN only.
- IKEv2 only supported in the site-to-site configuration
- Sophos XG v17 in bridge mode
- supports PPTP, L2TP, and OpenVPN.
- Cannot terminate the connection in bridge mode, promised in v18.
- Does not support roadwarrior IKEv2 even in gateway mode, also promised in v18
- Synology VPN Server
- Supports PPTP, L2TP, and OpenVPN.
- Limited to either Server or Client but not both (This kills many useful scenarios related to on-demand data replication so it is not suitable)
- Does not support IKEv2
While the OpenVPN route works reasonably well (see my previous post) and as a backup plan in case we hit a roadblock we can just set up second OpenVPN server it still has two little drawbacks:
- Requires OpenVPN app on iOS with atrocious UI and quality issues.
- Does not support an always-on scenario on managed devices as IKEv2 does.
Hence,
The Proposal
We shall set up an IPSEC solution with IKEv2.
What
Reviewing what’s available led me to consider Strongswan as pretty much the only candidate paired with Alpine Linux – lightweight and security-oriented distribution.
Where
I briefly considered raspberry pi: but decided against it due to low reliability and reluctance to add yet another device to the pile.
In Docker on Synology: IPsec will need to fiddle with low-level networking settings on the host OS which I don’t feel comfortable letting it do.
This leaves VM. Synology DSM offers VM Manager with fancy UI so this seems to be an obvious choice – all benefits that come with VM and reliability of RAID hardware, including snapshots and great performance.
How
The subsequent document will be a more or less step-by-step tutorial configuring and installing strongswan in Alpine Linux on Synology VM with IKEv2 with split-tunnel and full-tunnel support. Note Notes sections, these are important.
Caveats
Read Closing Notes for important details about the feasibility of automatic split DNS configuration: IKEv2 does not yet support payload types required to provide the clients with the private DNS configuration. This means for split DNS to work client-side configuration is unavoidable, at least for now.
Assumptions
We will make the following assumptions:
LAN: 10.0.17.0/24
VPN: 10.0.26.0/24
DNS: 10.0.17.1
Synology: 10.0.17.130
VM: 10.0.17.250
FDQN: vpn.example.com
User1: greg; [email protected]
User2: emili; [email protected]
IPv6: We don't bother for now
Creating Alpine Linux VM on Synology Diskstation 6
Prepare the VM
- Download Alpine Linux Virtual
x86_64
iso image and save to on the share. - Go to Package Center and install Virtual Machine Manager
- Create a new virtual machine
- Start Virtual Machine Manager.
- If it prompts to enable vSwitch – you can skip it here.
- Select
Virtual Machine
in the sidebar and clickCreate
- Select
Linux
orOther
and specify the following settings:- Name:
StrongSwan VM
- CPU(s): One is enough
- Memory:
128 Mb
- Video card: Does not matter.
- Storage location:
VM Storage 1
- Name:
- Click Next to go to the Storage page
- ISO File for bootup: Select iso we downloaded in step 1
- Virtual Disk:
256Mb
is more than enough. Default VirtIO controller is also fine.
- Click Next to go to the Network page.
- Select
Default VM Network
here.
- Select
- Next to go to Other Settings
- Autostart: Set to
Yes
. - Leave the rest as default.
- Autostart: Set to
- Next to the Permissions page
- Select users you want to be able to manage the VM. at least admin.
- Click next, check Power On and Apply.
- Start Virtual Machine Manager.
Configuring Alpine Linux
We’ll need to access the VM through Synology VM Manager provided frame buffer only once, to configure networking and enable SSH. After that subsequent configuration will be done via SSH directly on VM.
- Select the newly started virtual machine from the list and click
Connect
. Synology will open another browser window with access to the terminal of the client machine. - Login as
root
without password. - Execute setup_alpine. It will be asking a bunch of questions, answer as you wish, except important ones are outlined below
- Networking:
DHCP
- SSH daemon:
OpenSSH
- Installation Mode:
sys
- Disk:
/dev/sda
- Networking:
- Reboot, and log in again
- Create
~/.ssh/authorized_users
and place your public key there (you can just scp it from another machine). Don’t forget to set correct permissions:chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys
An alternative would be to uncomment
PasswordAuthentication yes
in/etc/ssh/sshd_config
, thenssh-copy-id
from another machine and disable password auth again. - While at it, configure the guest agent for Qemu to communicate with Synology VM host to facilitate snapshots safety among other things.
- Install by running
apk add qemu-guest-agent
- Configure device path: in the
/etc/conf.d/qemu-guest-agent
add lineGA_PATH="/dev/vport1p1"
- Start agent and verify the status:
service qemu-guest-agent start service qemu-guest-agent status
- Set runlevel to default for it to auto-start next time and verify
rc-update add qemu-guest-agent default rc-status
- Install by running
Now you can close the console and move over to your favorite terminal.
Installing StrongSwan
SSH to Alpine as root. It should authenticate with your SSH private key.
Install software:
apk --update add \
bash \
build-base \
curl \
curl-dev \
ca-certificates \
ip6tables \
iproute2 \
iptables-dev \
openssl \
openssl-dev
Download, build and install strongswan:
mkdir -p /tmp/strongswan
curl -Lo /tmp/strongswan.tar.bz2 \
https://download.strongswan.org/strongswan.tar.bz2
tar --strip-components=1 -C /tmp/strongswan -xjf /tmp/strongswan.tar.bz2
cd /tmp/strongswan
./configure --prefix=/usr \
--sysconfdir=/etc \
--libexecdir=/usr/lib \
--with-ipsecdir=/usr/lib/strongswan \
--enable-aesni \
--enable-chapoly \
--enable-cmd \
--enable-curl \
# --enable-dhcp \
# --enable-farp \
--enable-eap-dynamic \
--enable-eap-identity \
--enable-eap-md5 \
--enable-eap-mschapv2 \
--enable-eap-radius \
--enable-eap-tls \
--enable-files \
--enable-gcm \
--enable-md4 \
--enable-newhope \
--enable-ntru \
--enable-openssl \
--enable-sha3 \
--enable-shared \
--disable-aes \
--disable-des \
--disable-gmp \
--disable-hmac \
--disable-ikev1 \
--disable-md5 \
--disable-rc2 \
--disable-sha1 \
--disable-sha2 \
--disable-static && \
make && \
make install && \
cd
Cleanup:
rm -rf /tmp/*
apk del build-base curl-dev openssl-dev
rm -rf /var/cache/apk/*
Notes
Note DHCP and FARP are not enabled. Will explain later.
Preparing configuration files for IPsec
I’ve used these as a template.
/etc/ipsec.d/ipsec.conf
config setup
uniqueids=no
conn %default
keyexchange=ikev2
ikelifetime=60m
keylife=20m
rekeymargin=3m
keyingtries=1
rekey=no
ike=chacha20poly1305-prfsha256-newhope128,chacha20poly1305-prfsha256-ecp256,aes128gcm16-prfsha256-ecp256,aes256-sha256-modp2048,aes256-sha256-modp1024!
esp=chacha20poly1305-newhope128,chacha20poly1305-ecp256,aes128gcm16-ecp256,aes256-sha256-modp2048,aes256-sha256,aes256-sha1!
dpdaction=clear
dpddelay=120s
auto=add
conn roadwarrior-full
left=%any
# This is "Remote ID" that we'll use on the client to select connection. See notes below
leftid=@full-tunnel.vpn.example.com
leftauth=pubkey
leftcert=server_cert.pem
leftsendcert=always
leftsubnet=0.0.0.0/0
leftupdown=/etc/ipsec.d/firewall.updown
right=%any
rightauth=pubkey
rightsourceip=10.0.24.0/24
rightdns=10.0.17.1
# the only difference here we narrow down left subnet
conn roadwarrior-split
also=roadwarrior-full
leftid=@split-tunnel.vpn.example.com
leftsubnet=10.0.17.0/24
# These two if we want MSCHAPv2 EAP authentication
conn roadwarrior-eap-full
also=roadwarrior-full
rightauth=eap-dynamic
eap_identity=%any
conn roadwarrior-eap-split
also=roadwarrior-split
rightauth=eap-dynamic
eap_identity=%any
# These two for public key authentication
conn roadwarrior-pubkey-eap-full
also=roadwarrior-full
rightauth2=eap-dynamic
eap_identity=%any
conn roadwarrior-pubkey-eap-split
also=roadwarrior-split
rightauth2=eap-dynamic
eap_identity=%any
Notes
leftid
shall be mentioned in the server certificate, even though it is the fake name we just use to select the connection from the client.
@
means literal, as in “do not resolve”.
leftsubnet
defines the narrowed down networks and even ports and protocols if needed that implements split tunnel for us.
For rightsourceip
we have four choices in theory: let strongswan assign virtual IP or delegate that to a nearby DHCP server. In either case, we need to decide whether we want roadwarrior clients to become part of the same subnet or live on a separate virtual one.
Let’s consider each possibility:
Same subnet, no dhcp forwarding. We will need to ensure that the source IP range provided does not overlap with LAN’s DHCP server range.
Same subnet, dhcp plugin.
Setting rightsourceip=%dhcp
will tell Strongswan to forward DHCP requests to nearby DHCP server, configured separately. Since this situation is a bit confusing – virtual clients map all to the same mac address – we’ll need to enable FARP plugin (fake ARP?) for the VPN server to respond to ARP requests on behalf of VPN clients.
This setup would be perfect; DHCP options – such as search domain and suffix – get delivered to VPN clients seamlessly and because everything is handled by the single DHCP and DNS server local name resolution “just works”.
Unfortunately, this turned out to be far from rainbows and unicorns. It did not work very well, if at all, for no good reason, causing weird DHCP issues, including duplicate leases. Digging further I stumbled upon this comment on Ubiquiti forums that hints that this feature may be broken at the moment.
Different subnet, no dhcp. This works right away, but I don’t see a way to push DHCP options – DNS server and/or search domain – which makes the split-tunnel case worrisome.
Maybe not related – but for some reason, MacOS VPN config ignored DNS settings when I manually set them in the connection properties either. This is something that needs to be looked at separately.
As a side note – I could not get the IPsec to push the correct netmask to clients either – i.e. I send 10.0.22.0/24
but clients end up with 10.0.22.0/8
instead. This does not matter yet – as it still works – but it bothers me.
Different subnets, forward DHCP, no FARP. This will require setting up another DHCP server, perhaps on the same virtual machine; ensuring that it does not respond to requests from LAN and let it handle issuing virtual addresses and pushing options.
Now we have two DHCP servers and extra effort is needed to make local name resolution work across subnets
/etc/ipsec.d/ipsec.secrets
# VPN Server private key
: RSA server_key.pem
# EAP secrets if MSCHAPv2 is desired instead of certificates
greg : EAP "crazy-long-pass-if-greg-does-not-want-to-use-keys"
emili : EAP "even-longer-passw-for-similar-situation-that-might-arize"
# Users' private keys
: RSA greg_key.pem
: RSA emili_key.pem
/etc/strongswan.conf
charon {
send_vendor_id = yes
dns1 = 10.0.17.1
dns2 = 10.0.17.1
plugins {
eap-dynamic {
preferred = mschapv2, tls, md5
}
dhcp {
identity_lease = no
}
}
}
Notes
- identity_lease must be off – otherwise bad things happen – it would assign the same IP address if the same user connects from two devices. Perhaps it’s a bug but I’ve turned this off fir the time.
- It would be better to edit files under
/etc/strongswan.d/charon/
and include those above, but this is easier for testing.
/etc/sysctl.d/99-strongswan.conf
net.ipv4.ip_forward=1
/etc/ipsec.d/firewall.updown
This is copied entirely from the GitHub project I referenced above. It is important to include this if either server or client or both are behind NAT. The other way to handle it is to configure leftfirewall/rihgtfirewall options but those are deprecated(citation needed).
case $PLUTO_VERB in
up-client)
IF=$(ip r get ${PLUTO_PEER_CLIENT}|sed -ne 's,^.*dev \(\S\+\) .*,\1,p')
# NAT for using local IPV4 address in rightsourceip:
iptables -t nat -A POSTROUTING -s ${PLUTO_PEER_CLIENT} -o $IF -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -A POSTROUTING -s ${PLUTO_PEER_CLIENT} -o $IF -j MASQUERADE
;;
down-client)
IF=$(ip r get ${PLUTO_PEER_CLIENT}|sed -ne 's,^.*dev \(\S\+\) .*,\1,p')
# NAT for using local IPV4 address in rightsourceip:
iptables -t nat -D POSTROUTING -s ${PLUTO_PEER_CLIENT} -o $IF -m policy --dir out --pol ipsec -j ACCEPT
iptables -t nat -D POSTROUTING -s ${PLUTO_PEER_CLIENT} -o $IF -j MASQUERADE
;;
up-client-v6)
IF=$(ip -6 r get ${PLUTO_PEER_CLIENT%????}|sed -ne 's,^.*dev \(\S\+\) .*,\1,p')
# ARP proxy for using public IPv6 address in rightsourceip:
#ip -6 neigh add proxy ${PLUTO_PEER_CLIENT%????} dev $IF
# NAT for using local IPv6 address in rightsourceip:
ip6tables -t nat -A POSTROUTING -s ${PLUTO_PEER_CLIENT%????} -o $IF -m policy --dir out --pol ipsec -j ACCEPT
ip6tables -t nat -A POSTROUTING -s ${PLUTO_PEER_CLIENT%????} -o $IF -j MASQUERADE
;;
down-client-v6)
IF=$(ip -6 r get ${PLUTO_PEER_CLIENT%????}|sed -ne 's,^.*dev \(\S\+\) .*,\1,p')
# ARP proxy for using public IPv6 address in rightsourceip:
#ip -6 neigh delete proxy ${PLUTO_PEER_CLIENT%????} dev $IF
# NAT for using local IPv6 address in rightsourceip:
ip6tables -t nat -D POSTROUTING -s ${PLUTO_PEER_CLIENT%????} -o $IF -m policy --dir out --pol ipsec -j ACCEPT
ip6tables -t nat -D POSTROUTING -s ${PLUTO_PEER_CLIENT%????} -o $IF -j MASQUERADE
;;
esac
Setting up PKI and generating certificates
To generalize slightly let’s define some variables:
#!/bin/bash
# Country code and Org name
C="ZU"
O="Home Sweet Home"
# Root Certificate Configuration
CA_DN="C=$C, O=$O, CN=Greg and Emili Household Root CA"
CA_KEY="/etc/ipsec.d/private/ca_key.pem"
CA_CERT="/etc/ipsec.d/cacerts/ca_cert.pem"
# VPN Server Configuration
DOMAIN="example.com"
SERVER_SAN="vpn.$DOMAIN"
REMOTE_ID_FULL="full-tunnel.$SERVER_SAN"
REMOTE_ID_SPLIT="split-tunnel.$SERVER_SAN"
SERVER_DN="C=$C, O=$O, CN=$SERVER_SAN"
SERVER_KEY="/etc/ipsec.d/private/server_key.pem"
SERVER_CERT="/etc/ipsec.d/certs/server_cert.pem"
PKI generation. Note multiple --san
arguments to support selectors.
echo "Generating private key for CA"
ipsec pki --gen --outform pem > "$CA_KEY"
echo "Generating self-signed certificate for the CA"
ipsec pki --self \
--in "$CA_KEY" \
--dn "$CA_DN" \
--ca --outform pem > "$CA_CERT"
echo "Generating private key for the VPN server"
ipsec pki --gen --outform pem > "$SERVER_KEY"
echo "Generating and signing x509 certificate for the server"
ipsec pki --issue \
--in "$SERVER_KEY" --type priv \
--cacert "$CA_CERT" --cakey "$CA_KEY" \
--dn "$SERVER_DN" \
--san="$SERVER_SAN" \
--san="$REMOTE_ID_FULL" \
--san="$REMOTE_ID_SPLIT" \
--flag serverAuth --flag ikeIntermediate \
--outform pem > "$SERVER_CERT"
And now generate client certificates. Pack them to p12 while at it for ease of deployment.
function generate_client(){
name="$1"
keyname="${name}_key.pem"
certname="${name}_cert.pem"
p12name="${name}_cert.p12"
CLIENT_CN="${name}@$DOMAIN"
echo "Generting private key for the user $name"
ipsec pki --gen \
--outform pem > /etc/ipsec.d/private/"$keyname"
echo "Generting and signing certificate for the user $name"
ipsec pki --issue \
--in /etc/ipsec.d/private/"$keyname" \
--type priv \
--cacert "$CA_CERT" --cakey "$CA_KEY" \
--dn "C=$C, O=$O, CN=$CLIENT_CN" \
--san="$CLIENT_CN" \
--outform pem > /etc/ipsec.d/certs/"$certname"
echo "Exporting p12 for the user $name"
openssl pkcs12 -export \
-inkey /etc/ipsec.d/private/"$keyname" \
-in /etc/ipsec.d/certs/"$certname" \
-name "$CLIENT_CN" \
-certfile "$CA_CERT" \
-caname "$CN" \
-out /etc/ipsec.d/"$p12name"
}
echo Generating Clients
generate_client "greg"
generate_client "emili"
Notes
- Above we set
leftsendcert=always
so we don’t need to distribute the server certificate. We only need to deploy the Root CA certificate. - During the generation of client certificates you’ll be prompted for an encryption passphrase to protect users’ keys. Save them and provide them to the users separately from the p12 files.
- Retrieve Root CA certificate and client p12 files along with export passwords for Certificate-based authentication or CHAP secrets for MSCHAPv2.
Network infrastructure configuration
DHCP
Setup your DHCP server to issue the same address to the VPN Server. How to do that depends on your DHCP server.
DNS
If external IP is not static and DDNS has not been set up configure DDNS client to update vpn.example.com
to your gateway
Routing
On your gateway configure routing rules to send traffic destined to VPN subnet to the VM instance. This will allow you to access devices over VPN from your LAN
Port forwarding
Forward IP security ports udp/500
and udp/4500
to VPN Server and allow AH
, ESP
, IKE
traffic to VPN server
Configuring client devices
MacOS
Setting up VPN IKEv2 network connection in System Preferences -> Network should be straightforward and it works great in Full tunnel case.
For the split-tunnel case while the IP routing works correctly it is not clear how to make split-DNS work seamlessly enough, without manual client-side configuration. See Closing Notes for details.
There are few ways I’ve tried to get DNS search suffix and resolver pushed/configured, including setting up dedicated dnsmasq server for virtual clients with %dhcp
option; however, this did not result in anything but longer connection setup time; ultimately DNS server and search domain pushed that way would only affect scoped queries which is not very useful.
The alternative is to keep using ispec to virtual IP addresses (and get rid of the extra complexity associated with the additional DHCP server. I, therefore, commented out the DHCP plugin; alternatively one could tell charon what plugins to load without recompiling it) and attempt to make necessary changes to support split-channel on the client.
I did not yet find out how to force macOS to first search resolver for the VPN network - i.e. for ping chipmunk
to result in query for chipmunk.home.example.com
; but I did find a way for at least FDQN resolution to work:
Create /etc/resolver/
directory and place file names home.example.com
inside with the content pointing to the nameserver: nameserver 10.0.17.1
.
This however breaks the resolution of home.example.com itself - so if that is FDQN of your server - you’ll have to remove that configuration when VPN is down.
It would be great to have a properly implemented DNS-aware client for IKEv2 for MacOS such as Viscosity or VPN Tracker that will properly handle this - if you know one let me know.
iOS
Configuring is also straightforward - import the p12 file to use key-based authentication or use EAP secrets. This works very well, the tunnel is set up almost instantly and is fairly quick, and works flawlessly with full tunnel mode.
Split-tunnel also works just fine, however not split-DNS. I don’t have a working solution from the Split-DNS case yet.
Debugging
To immediately see what’s going on stop the service and run it with --debug
and --nofork
options.
ipsec stop
ipsec start --nofork --debug
The output is fairly verbose I found there is no need to tweak logging level to resolve most of the connectivity/certificate/authentication issues.
Closing notes
This seems to work very well for full tunnel scenarios. Split tunnels also work, however, there is no clear path to make split-DNS work in a friendly way (on a desktop) or at all (on mobiles).
There is a draft proposal called Split DNS Configuration for IKEv2 to add payload attribute types INTERNAL_DNS_DOMAIN
and INTERNAL_DNSSEC_TA
to address this. I guess we’ll have to wait.
References
- alpine-strongswan-vpn
- Alpine Linux Installation
- Forwarding and split tunneling
- Windows 7 Certificate Requirements
- ietf proposal for Split DNS Configuration for IKEv2
History
March 25, 2018 | initial publication |
April 25, 2018 | Added Qemu guest agent configuration |
March 14, 2019 | Clarified split-tunnel and split-DNS support |