Last Updated Mar 14, 2019

Background

I needed to have a VPN server at home that I would connect to from anywhere to enjoy:

  1. Security of encrypted tunnel
  2. Access to Web content filter deployed at home
  3. Access to machines in my home network as needed

Requirements

Nothing unreasonable here:

  1. Configuration on a client should be simple. Ideally – one-click.
  2. Connection should be automatic, resilient to network interruptions, and should support always-on.
  3. Routing between LAN and VPN devices shall work
  4. DNS resolution shall work
  5. Certificate-based authentication desirable. EAP will also do.
  6. Support for full tunnel and split tunnel, including split-DNS mode, and quick switching between the two.
  7. 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 click Create
    • Select Linux or Other 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
    • 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.
    • Next to go to Other Settings
      • Autostart: Set to Yes.
      • Leave the rest as default.
    • 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.

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
  • 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, then ssh-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 line
      GA_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
      

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.

Bummer.

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

  1. 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.
  2. 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

  1. Above we set leftsendcert=always so we don’t need to distribute the server certificate. We only need to deploy the Root CA certificate.
  2. 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.
  3. 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

History

March 25, 2018 initial publication
April 25, 2018 Added Qemu guest agent configuration
March 14, 2019 Clarified split-tunnel and split-DNS support