How to Connect to an L2TP/IPsec VPN from Linux
I needed to connect a Linux client to an L2TP/IPsec VPN using shared secret (a.k.a. pre-shared key) authentication, but every online guide I could find was inaccurate and/or incomplete. Here’s a fully working solution at the time of writing.
The initial 10-minute setup is pretty tedious, but once you’ve got it working you can throw it all into a script and never worry about it again.
Update A less-than-ideal but much easier fix is to build xl2tpd from source with one problematic line commented out and save
: PSK "sh4r3ds3cr3t"
to/etc/ipsec.secrets
. This allows the use of Libreswan and NetworkManager, although on Arch Linux I also had tosudo mkdir -p /var/lib/ipsec/nss
since that directory wasn’t created during Libreswan installation (?!)
Walkthrough
Let’s say you want to connect to a VPN server with public IP address 68.68.32.79
and shared secret sh4r3ds3cr3t
. Your user account on the VPN server is johndoe
with password j0hn5p455w0rd
.
Install Openswan for IPsec and xl2tpd for L2TP:
# Arch Linux pacaur -S openswan xl2tpd # Ubuntu apt-get update && apt-get install openswan xl2tpd
Run
ip route show default
to determine your local IP and default network interface. They should be the 3rd and 5th items in the single line of output. Let’s say they’re192.168.1.1
andeth0
.As root, create these four files, plugging in the six variables we’ve gotten so far:
/etc/ipsec.conf
version 2.0 config setup virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12 nat_traversal=yes protostack=netkey plutoopts="--interface=eth0" conn L2TP-PSK authby=secret pfs=no auto=add keyingtries=3 dpddelay=30 dpdtimeout=120 dpdaction=clear rekey=yes ikelifetime=8h keylife=1h type=transport left=%defaultroute leftnexthop=%defaultroute leftprotoport=17/1701 right=68.68.32.79
/etc/ipsec.secrets
0.0.0.0 68.68.32.79: PSK "sh4r3ds3cr3t"
/etc/xl2tpd/xl2tpd.conf
[lac vpn-connection] lns = 68.68.32.79 refuse chap = yes refuse pap = yes require authentication = yes name = vpn-server ppp debug = yes pppoptfile = /etc/ppp/options.l2tpd.client length bit = yes
/etc/ppp/options.l2tpd.client
ipcp-accept-local ipcp-accept-remote refuse-eap require-mschap-v2 noccp noauth idle 1800 mtu 1410 mru 1410 defaultroute usepeerdns debug connect-delay 5000 name johndoe password j0hn5p455w0rd
Start Openswan and xl2tpd. These three commands, along with all other commands in the remainder of this walkthrough, should be run as root:
ipsec setup --start xl2tpd -D & ipsec auto --up L2TP-PSK
Connect to the VPN:
echo 'c vpn-connection' > /var/run/xl2tpd/l2tp-control
Determine the name of the PPP network interface by running
ip address
and looking for the entry containingppp
. Let’s say it’s calledppp0
. Now look inside its entry for the word “peer” and the local IP that immediately follows it. Let’s say the peer IP is10.1.14.252
. This is the VPN server’s local IP.Add a rule to route subnet traffic through the VPN.
10.0.0.0/8
is CIDR notation for the range of local IPs assigned inside your remote network. It doesn’t need to be exact. For example, you can use10.0.0.0/8
to specify that all IPs of the form10.*.*.*
belong to the VPN even if you suspect that your remote network only assigns IPs of the form10.1.10.*
and10.1.14.*
.ip route add 10.0.0.0/8 via 10.1.14.252 dev ppp0
You can test the new rule by pinging or SSHing the local IP of another machine inside the VPN. If that’s all you need, you can stop here.
Remember the variables we found in step 2? Use them here to route all internet traffic (not just subnet traffic) through the VPN:
ip route add 68.68.32.79 via 192.168.1.1 dev eth0 ip route delete default via 192.168.1.1 dev eth0 ip route add default via 10.1.14.252 dev ppp0
Done! You can now open your web browser and access VPN-only resources. To disconnect:
Undo the routing rules:
ip route delete default via 10.1.14.252 dev ppp0 ip route add default via 192.168.1.1 dev eth0 ip route delete 68.68.32.79 via 192.168.1.1 dev eth0
Kill the VPN connection, xl2tpd, and Openswan:
echo 'd vpn-connection' > /var/run/xl2tpd/l2tp-control ipsec auto --down L2TP-PSK killall xl2tpd ipsec setup --stop
Caveats
I don’t recommend switching from Ethernet to Wi-Fi or vice versa while connected to the VPN since that will change the default network interface (eth0
in our example) and possibly leave your routing table in a weird state until reboot.
These instructions assume that you regularly use only one L2TP/IPsec VPN. It’s straightforward to simply add a new section to the .conf
files for each additional VPN, but the script below doesn’t support that use case out of the box.
I’ve only tested this on Arch Linux. Let me know if your distro needs any tweaks!
Script
Once you’ve successfully executed the steps above by hand, you can automate them all using a script like the one below. I normally run it as sudo ./vpn toggle
.
#!/usr/bin/env bash
#
# Enables or disables a client connection to an L2TP/IPsec VPN. Usage: save
# this file as `vpn`, `chmod +x` it, and run `./vpn [up/down/toggle]` as root.
# Requires openswan and xl2tpd packages. Tested on Arch Linux.
#
# Based on:
# https://bbs.archlinux.org/viewtopic.php?pid=1773313#p1773313
# https://bbs.archlinux.org/viewtopic.php?pid=1781882#p1781882
#
# Example `ip route` output before connecting to VPN:
# default via 192.168.1.1 dev wlp4s0 proto dhcp src 192.168.1.7 metric 303
# 10.1.14.252 dev ppp0 proto kernel scope link src 10.1.16.107
# 192.168.1.0/24 dev wlp4s0 proto dhcp scope link src 192.168.1.7 metric 303
#
# Example `ip route` output after connecting to VPN:
# default via 10.1.14.252 dev ppp0
# 10.0.0.0/8 via 10.1.14.252 dev ppp0
# 10.1.14.252 dev ppp0 proto kernel scope link src 10.1.16.107
# 71.245.184.58 via 192.168.1.1 dev wlp4s0
# 192.168.1.0/24 dev wlp4s0 proto dhcp scope link src 192.168.1.7 metric 303
set -eu
# VPN settings (edit these!)
vpn_server_public_ip='68.68.32.79'
vpn_subnet='10.0.0.0/8'
vpn_pingee_local_ip='10.1.10.22'
vpn_shared_secret='sh4r3ds3cr3t'
vpn_username='johndoe'
vpn_password='j0hn5p455w0rd'
# Ensure that we're running as root
if [[ "$(id -u)" != 0 ]]; then
echo 'Must run as root!'
exit 1
fi
# Ensure that required packages are installed
if ! command -v ipsec > /dev/null; then
echo '`ipsec` command not found! Please install Openswan.'
exit 1
fi
if ! command -v xl2tpd > /dev/null; then
echo '`xl2tpd` command not found! Please install xl2tpd.'
exit 1
fi
# Handle subcommands
subcommand="${1-}"
if [[ "${subcommand}" = 'toggle' ]]; then
if ip address | grep -q ': ppp'; then
subcommand='down'
else
subcommand='up'
fi
fi
case "${subcommand}" in
# Connect to VPN
up)
# Ensure that we're not already connected
if ip address | grep -q ': ppp'; then
echo 'Already connected to VPN!'
exit 1
fi
# Write config files
default_network_device="$(ip route show default | cut -d' ' -f5)"
cat > /etc/ipsec.conf << EOF
version 2.0
config setup
virtual_private=%v4:10.0.0.0/8,%v4:192.168.0.0/16,%v4:172.16.0.0/12
nat_traversal=yes
protostack=netkey
plutoopts="--interface=${default_network_device}"
conn L2TP-PSK
authby=secret
pfs=no
auto=add
keyingtries=3
dpddelay=30
dpdtimeout=120
dpdaction=clear
rekey=yes
ikelifetime=8h
keylife=1h
type=transport
left=%defaultroute
leftnexthop=%defaultroute
leftprotoport=17/1701
right=${vpn_server_public_ip}
EOF
cat > /etc/ipsec.secrets << EOF
0.0.0.0 ${vpn_server_public_ip}: PSK "${vpn_shared_secret}"
EOF
cat > /etc/xl2tpd/xl2tpd.conf << EOF
[lac vpn-connection]
lns = ${vpn_server_public_ip}
refuse chap = yes
refuse pap = yes
require authentication = yes
name = vpn-server
ppp debug = yes
pppoptfile = /etc/ppp/options.l2tpd.client
length bit = yes
EOF
cat > /etc/ppp/options.l2tpd.client << EOF
ipcp-accept-local
ipcp-accept-remote
refuse-eap
require-mschap-v2
noccp
noauth
idle 1800
mtu 1410
mru 1410
defaultroute
usepeerdns
debug
connect-delay 5000
name ${vpn_username}
password ${vpn_password}
EOF
# Start Openswan
ipsec setup --start
ipsec setup --status | grep -q 'IPsec running'
# Start xl2tpd
xl2tpd -D &
sleep 1
ipsec auto --up L2TP-PSK
ipsec setup --status | grep -q '1 tunnels up'
# Connect to VPN
echo 'c vpn-connection' > /var/run/xl2tpd/l2tp-control
while true; do
sleep 1
ppp_device="$(ip address | grep -oE '^[0-9]+: ppp\w+' | cut -d' ' -f2)"
if [[ -n "${ppp_device}" ]]; then
break
fi
echo 'Waiting for ppp device...'
done
peer_ip="$(ip address show "${ppp_device}" | grep -oE 'peer [0-9.]+' | cut -d' ' -f2)"
ip route add "${vpn_subnet}" via "${peer_ip}" dev "${ppp_device}"
[[ -z "${vpn_pingee_local_ip}" ]] || ping -c 1 "${vpn_pingee_local_ip}"
# Route all internet traffic through VPN
local_ip="$(ip route show default | cut -d' ' -f3)"
ip route add "${vpn_server_public_ip}" via "${local_ip}" dev "${default_network_device}"
ip route delete default via "${local_ip}" dev "${default_network_device}"
ip route add default via "${peer_ip}" dev "${ppp_device}"
;;
# Disconnect from VPN
down)
# Ensure that we're already connected
if ! ip address | grep -q ': ppp'; then
echo 'Not connected to VPN!'
exit 1
fi
# Undo routing rules
default_network_device="$(ip route show "${vpn_server_public_ip}" | cut -d' ' -f5)"
ppp_device="$(ip address | grep -oE '^[0-9]+: ppp\w+' | cut -d' ' -f2)"
peer_ip="$(ip address show "${ppp_device}" | grep -oE 'peer [0-9.]+' | cut -d' ' -f2)"
local_ip="$(ip route show "${vpn_server_public_ip}" | cut -d' ' -f3)"
ip route delete default via "${peer_ip}" dev "${ppp_device}"
ip route add default via "${local_ip}" dev "${default_network_device}"
ip route delete "${vpn_server_public_ip}" via "${local_ip}" dev "${default_network_device}"
# Kill VPN connection
echo 'd vpn-connection' > /var/run/xl2tpd/l2tp-control
sleep 1
# Kill xl2tpd
ipsec auto --down L2TP-PSK
killall xl2tpd
sleep 1
# Kill Openswan
ipsec setup --stop
;;
*)
echo 'Invalid subcommand!'
exit 1
;;
esac
echo 'Success!'