Skip to content
 

“On the fly” IPsec VPN with iproute2

(This has been on the TODO list for months, let's finally get to it.)

Basically we're going to create an IPsec VPN with static manual keys, using only the ip command from iproute2.

As there seems to be some confusion, note that the VPN we're setting up here has nothing to do with a PSK setup, in which normally there is an IKE daemon that dynamically computes the keys and generates new ones at regular intervals. The PSK is used for IKE authentication purposes, and after that the actual IPsec keys are truly dynamic and change periodically.

Here instead we're not using IKE; we just manually generate some static IPsec keys (technically, we generate a bunch of security associations (SAs); each SA has a number of properties, among which is the SPI - a number that identifies the SA - and two keys, one used for authentication/integrity check and the other for enctryption).
As should be clear, this is not something to be used on a regular basis and/or for a long time; it's rather a quick and dirty hack to be used in emergency situations. The problem with passing a lot of VPN traffic using the same keys should be apparent: the more data encrypted with the same key an attacker has, the higher the chances of a successful decryption (which would affect all traffic, including past one). Manual keying also has other problems, like the lack of peer authentication and the possibility of replay attacks.
On the plus side, it's very easy to setup and it does not require to install any software; the only prerequisites are iproute2 (installed by default on just about any distro nowadays) and a kernel that supports IPsec (ditto).

So here's the sample topology:

ipsectun

So, quite obviusly, we want traffic between 10.0.0.0/24 and 172.16.0.0/24 to be encrypted.

There are two important concepts in IPsec: the SPD (Security Policy Database) and the SAs (Security Association). A SA contains the actual security parameters and keys to be used between two given peers; since a SA is unidirectional, there will always be two of them, one for each direction of the traffic. The SPD, on the other hand, defines to which traffic the SA should be applied, in terms of source/destination IP ranges and protocols. If a packet matches a SPD, the associated SA is applied to it, resulting in its encryption, signing or whatever the SA prescribes.
For them to be of any use, the SPD and the SAs must match (with reversed source/destination values) at both ends.

In our case, we're going to manually define both the SPD policies and the SAs using the xfrm subcommand of the ip utility. (In a traditional IKE setup, instead, the security policies are statically defined in configuration files and the SAs are dynamically established by IKE either at startup or upon seeing matching traffic, and periodically renewed.)

The idea here is to have some code that outputs the commands to be run on GW1 and GW2 respectively, so they can be copied and pasted.

Since authentication and encryption keys are an essential part of a SA, let's start by generating them. We want to use AES256 to encrypt, and SHA256 for integrity check, so we know the key length: 256 bit, or 32 bytes. Each SA contains two keys, and there will be two SAs, so we generate four keys.

# bash
declare -a keys
for i in {1..4}; do
  # keys 1 and 3 are for HMAC, keys 2 and 4 are for encryption
  keys[i]=$(xxd -p -l 32 -c 32 /dev/random)
done

As usual, if reading from /dev/random blocks, you need to add some entropy to the pool or use /dev/urandom (less secure, but this setup isn't meant to be very secure anyway).
Each SA needs a unique ID, called the SPI (Security Parameter Index), which is 32 bits (4 bytes) long:

declare -a spi
for i in {1..2}; do
  spi[i]=$(xxd -p -l 4 /dev/random)
done

Finally, there has to be what iproute calls the reqid, which is what links a SA with a SPD. Again this is 4 bytes, so let's generate it (each SA has its own reqid):

declare -a reqid
for i in {1..2}; do
  reqid[i]=$(xxd -p -l 4 /dev/random)
done

The code to create the SAs is as follows (same code for GW1 and GW2):

ip xfrm state add src 192.0.2.1 dst 198.51.100.20 proto esp spi "0x${spi[1]}" reqid "0x${reqid[1]}" mode tunnel auth sha256 "0x${keys[1]}" enc aes "0x${keys[2]}"
ip xfrm state add src 198.51.100.20 dst 192.0.2.1 proto esp spi "0x${spi[2]}" reqid "0x${reqid[2]}" mode tunnel auth sha256 "0x${keys[3]}" enc aes "0x${keys[4]}"

Now to the SPD. Here we define which traffic should be encrypted. In our case, of course, it's all traffic between 10.0.0.0/24 and 172.16.0.0/24, in both directions.

# for GW1
ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir out tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid "0x${reqid[1]}" mode tunnel
ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir fwd tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid "0x${reqid[2]}" mode tunnel
ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir in tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid "0x${reqid[2]}" mode tunnel

# for GW2
ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir out tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid "0x${reqid[2]}" mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir fwd tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid "0x${reqid[1]}" mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir in tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid "0x${reqid[1]}" mode tunnel

The commands are symmetrical, with src/dst pairs swapped. I'm not 100% sure why a fourth policy in the "fwd" direction is not needed (more information welcome), but looking at what eg openswan does, it seems that it creates only three policies as above and everything works, so let's stick with that.

The last thing to do is to add suitable routes for the traffic that must be encrypted:

# for GW1
ip route add 172.16.0.0/24 dev eth0 src 10.0.0.1
# for GW2
ip route add 10.0.0.0/24 dev eth0 src 172.16.0.1

Specifying the "src" parameter is important here, if we want traffic originating on the gateways themselves to go through the tunnel.

Now any traffic between the two networks 10.0.0.0/24 and 172.16.0.0/24 will go through the VPN.

So here is the complete script, with endpoint and subnet addresses parametrized (yes, argument checking - and not only that - could certainly be better):

#!/bin/bash

# doipsec.sh

dohelp(){
  echo "usage: $0 <GW1_public_IP|GW1_range|GW1_internal_IP[|GW1_public_iface[|GW1_GWIP]]> <GW2_public_IP|GW2_range|GW2_internal_IP[|GW2_public_iface[|GW2_GWIP]]>" >&2
  echo "Output commands to set up an ipsec tunnel between two machines" >&2
  echo "Example: $0 '192.0.2.1|10.0.0.0/24|10.0.0.254|eth0' '198.51.100.20|172.16.0.0/24|172.16.0.1|eth1'" >&2
}

if [ $# -ne 2 ] || [ "$1" = "-h" ]; then
  dohelp
  exit 1
fi

IFS="|" read -r GW1_IP GW1_NET GW1_IIP GW1_IF GW1_GWIP <<< "$1"
IFS="|" read -r GW2_IP GW2_NET GW2_IIP GW2_IF GW2_GWIP <<< "$2"

if [ "${GW1_IP}" = "" ] || [ "${GW1_NET}" = "" ] || [ "${GW1_IIP}" = "" ]; then
  dohelp
  exit 1
fi

# assume eth0 if not specified
[ "${GW1_IF}" = "" ] && GW1_IF=eth0

# generate variable data

rand_device=/dev/random    # change to urandom if needed

declare -a keys
for i in {1..4}; do
  # keys 1 and 3 are for HMAC, keys 2 and 4 are for encryption
  keys[i]=$(xxd -p -l 32 -c 32 "${rand_device}")
done

declare -a spi
for i in {1..2}; do
  spi[i]=$(xxd -p -l 4 "${rand_device}")
done

declare -a reqid
for i in {1..2}; do
  reqid[i]=$(xxd -p -l 4 "${rand_device}")
done

# route statement to allow default routing through the tunnel

# sucking heuristic
if [ "${GW1_GWIP}" != "" ] && [ "${GW2_NET}" = "0.0.0.0/0" ]; then
  # add a /32 route to the peer before pointing the default to the tunnel
  GW1_GW2_ROUTE="ip route add ${GW2_IP}/32 dev ${GW1_IF} via ${GW1_GWIP} && ip route del ${GW2_NET} && ip route add ${GW2_NET} dev ${GW1_IF} src ${GW1_IIP}" 
else
  GW1_GW2_ROUTE="ip route add ${GW2_NET} dev ${GW1_IF} src ${GW1_IIP}" 
fi

if [ "${GW2_GWIP}" != "" ] && [ "${GW1_NET}" = "0.0.0.0/0" ]; then
  GW2_GW1_ROUTE="ip route add ${GW1_IP}/32 dev ${GW2_IF} via ${GW2_GWIP} && ip route del ${GW1_NET} && ip route add ${GW1_NET} dev ${GW2_IF} src ${GW2_IIP}" 
else
  GW2_GW1_ROUTE="ip route add ${GW1_NET} dev ${GW2_IF} src ${GW2_IIP}" 
fi

cat << EOF
**********************
Commands to run on GW1
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src ${GW1_IP} dst ${GW2_IP} proto esp spi 0x${spi[1]} reqid 0x${reqid[1]} mode tunnel auth sha256 0x${keys[1]} enc aes 0x${keys[2]}
ip xfrm state add src ${GW2_IP} dst ${GW1_IP} proto esp spi 0x${spi[2]} reqid 0x${reqid[2]} mode tunnel auth sha256 0x${keys[3]} enc aes 0x${keys[4]}

ip xfrm policy add src ${GW1_NET} dst ${GW2_NET} dir out tmpl src ${GW1_IP} dst ${GW2_IP} proto esp reqid 0x${reqid[1]} mode tunnel
ip xfrm policy add src ${GW2_NET} dst ${GW1_NET} dir fwd tmpl src ${GW2_IP} dst ${GW1_IP} proto esp reqid 0x${reqid[2]} mode tunnel
ip xfrm policy add src ${GW2_NET} dst ${GW1_NET} dir in tmpl src ${GW2_IP} dst ${GW1_IP} proto esp reqid 0x${reqid[2]} mode tunnel

${GW1_GW2_ROUTE}

**********************
Commands to run on GW2
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src ${GW1_IP} dst ${GW2_IP} proto esp spi 0x${spi[1]} reqid 0x${reqid[1]} mode tunnel auth sha256 0x${keys[1]} enc aes 0x${keys[2]}
ip xfrm state add src ${GW2_IP} dst ${GW1_IP} proto esp spi 0x${spi[2]} reqid 0x${reqid[2]} mode tunnel auth sha256 0x${keys[3]} enc aes 0x${keys[4]}

ip xfrm policy add src ${GW2_NET} dst ${GW1_NET} dir out tmpl src ${GW2_IP} dst ${GW1_IP} proto esp reqid 0x${reqid[2]} mode tunnel
ip xfrm policy add src ${GW1_NET} dst ${GW2_NET} dir fwd tmpl src ${GW1_IP} dst ${GW2_IP} proto esp reqid 0x${reqid[1]} mode tunnel
ip xfrm policy add src ${GW1_NET} dst ${GW2_NET} dir in tmpl src ${GW1_IP} dst ${GW2_IP} proto esp reqid 0x${reqid[1]} mode tunnel

${GW2_GW1_ROUTE}
EOF

So for our example we'd run it with something like:

$mdoipsec.sh '192.0.2.1|10.0.0.0/24|10.0.0.254|eth0' '198.51.100.20|172.16.0.0/24|172.16.0.1|eth1'
**********************
Commands to run on GW1
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src 192.0.2.1 dst 198.51.100.20 proto esp spi 0xfd51141e reqid 0x62502e58 mode tunnel auth sha256 0x4046c2f9ff22725b850e2d981968249dc6c25fba189e701cf9a14e921f91cffb enc aes 0xccd80053ae1b55113a89bc476d0de1d9e8b7bc94655f3af1b0dad7bb9ada1065
ip xfrm state add src 198.51.100.20 dst 192.0.2.1 proto esp spi 0x34e0aac0 reqid 0x66a32a19 mode tunnel auth sha256 0x1caf04f262e889b9b53b6c95bfbb4ef0292616362e8878fe96123610ca000892 enc aes 0x9380e038247fcd893d4f8799389b90bfa4d0b09195495bb94fe3a9fa5c5b699d

ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir out tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0x62502e58 mode tunnel
ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir fwd tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x66a32a19 mode tunnel
ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir in tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x66a32a19 mode tunnel

ip route add 172.16.0.0/24 dev eth0 src 10.0.0.254

**********************
Commands to run on GW2
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src 192.0.2.1 dst 198.51.100.20 proto esp spi 0xfd51141e reqid 0x62502e58 mode tunnel auth sha256 0x4046c2f9ff22725b850e2d981968249dc6c25fba189e701cf9a14e921f91cffb enc aes 0xccd80053ae1b55113a89bc476d0de1d9e8b7bc94655f3af1b0dad7bb9ada1065
ip xfrm state add src 198.51.100.20 dst 192.0.2.1 proto esp spi 0x34e0aac0 reqid 0x66a32a19 mode tunnel auth sha256 0x1caf04f262e889b9b53b6c95bfbb4ef0292616362e8878fe96123610ca000892 enc aes 0x9380e038247fcd893d4f8799389b90bfa4d0b09195495bb94fe3a9fa5c5b699d

ip xfrm policy add src 172.16.0.0/24 dst 10.0.0.0/24 dir out tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x66a32a19 mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir fwd tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0x62502e58 mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 172.16.0.0/24 dir in tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0x62502e58 mode tunnel

ip route add 10.0.0.0/24 dev eth1 src 172.16.0.1

Note that it's not necessarily the two networks local to GW1 and GW2 that have to be connected by the tunnel. If GW2 had, say, an existing route to 192.168.0.0/24, it would be perfectly possible to say:

$ doipsec.sh '192.0.2.1|10.0.0.0/24|10.0.0.254|eth0' '198.51.100.20|192.168.0.0/24|172.16.0.1|eth1'
...

to encrypt traffic from/to 10.0.0.0/24 and 192.168.0.0/24. Of course, in this case either hosts in 192.168.0.0/24 must somehow have a route back to 10.0.0.0/24 going through GW2, or GW2 must NAT traffic coming from 10.0.0.0/24 destined to 192.168.0.0/24 (and hosts there must still have a route back to GW2's masquerading address), but that should be obvious.

In the same way, it's possible to just route everything to/from site A through the tunnel (although I would not recommend it):

$ doipsec.sh '192.0.2.1|10.0.0.0/24|10.0.0.254|eth0|192.0.2.254' '198.51.100.20|0.0.0.0/0|172.16.0.1|eth1'
**********************
Commands to run on GW1
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src 192.0.2.1 dst 198.51.100.20 proto esp spi 0x00127764 reqid 0xd7d184b1 mode tunnel auth sha256 0x8dcce7d80f7c8bb81e6a526b9d5d7ce2e7a474e3406c40953108b6d92b61cb77 enc aes 0xf9d41041fc014b94d602ed051800601464cdbc525847d5894ed03f55b8b5e78c
ip xfrm state add src 198.51.100.20 dst 192.0.2.1 proto esp spi 0xec8fe8cb reqid 0x18fcbfd1 mode tunnel auth sha256 0xc1dbbafc0deff6d4bfe0e2736d443d94ffe25ce8637e6f70e3260c87cf8f9724 enc aes 0x6170cd164092554bfd8402c528439c2c3d9823b74b493d9c18ca05a9c3b40a0d

ip xfrm policy add src 10.0.0.0/24 dst 0.0.0.0/0 dir out tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0xd7d184b1 mode tunnel
ip xfrm policy add src 0.0.0.0/0 dst 10.0.0.0/24 dir fwd tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x18fcbfd1 mode tunnel
ip xfrm policy add src 0.0.0.0/0 dst 10.0.0.0/24 dir in tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x18fcbfd1 mode tunnel

ip route add 198.51.100.20/32 dev eth0 via 192.0.2.254 && ip route del 0.0.0.0/0 && ip route add 0.0.0.0/0 dev eth0 src 10.0.0.254

**********************
Commands to run on GW2
**********************

ip xfrm state flush; ip xfrm policy flush

ip xfrm state add src 192.0.2.1 dst 198.51.100.20 proto esp spi 0x00127764 reqid 0xd7d184b1 mode tunnel auth sha256 0x8dcce7d80f7c8bb81e6a526b9d5d7ce2e7a474e3406c40953108b6d92b61cb77 enc aes 0xf9d41041fc014b94d602ed051800601464cdbc525847d5894ed03f55b8b5e78c
ip xfrm state add src 198.51.100.20 dst 192.0.2.1 proto esp spi 0xec8fe8cb reqid 0x18fcbfd1 mode tunnel auth sha256 0xc1dbbafc0deff6d4bfe0e2736d443d94ffe25ce8637e6f70e3260c87cf8f9724 enc aes 0x6170cd164092554bfd8402c528439c2c3d9823b74b493d9c18ca05a9c3b40a0d

ip xfrm policy add src 0.0.0.0/0 dst 10.0.0.0/24 dir out tmpl src 198.51.100.20 dst 192.0.2.1 proto esp reqid 0x18fcbfd1 mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 0.0.0.0/0 dir fwd tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0xd7d184b1 mode tunnel
ip xfrm policy add src 10.0.0.0/24 dst 0.0.0.0/0 dir in tmpl src 192.0.2.1 dst 198.51.100.20 proto esp reqid 0xd7d184b1 mode tunnel

ip route add 10.0.0.0/24 dev eth1 src 172.16.0.1

In this last case, GW2 must obviously perform NAT on at least some of the traffic coming from 10.0.0.0/24. IMPORTANT: since the fifth argument has been specified for GW1 and the remote network is 0.0.0.0/0, the resulting commands include a statement that temporarily deletes the default route on GW1, before recreating it to point into the tunnel. If you're running the commands remotely (eg via SSH) on the relevant machine, things can go wrong and screw up pretty easily. You must always inspect the generated routing code to make sure it's fine for your case, and take the necessary precautions to avoid losing access. This code isn't meant to be production-level anyway.

Another point worth noting is that the generated commands

ip xfrm state flush; ip xfrm policy flush

will remove any trace of IPsec configuration, including preexisting tunnels that may be configured and possibly running. But if that is the case, it means there is a "real" IPsec implementation on the machine, so that's what should be used for the new tunnel too, not the kludgy script described here.

So that's it for this klu^Wexperiment. In principle, one could envision some sort of scheduled task syncronized between the machines that updates the SAs or generates new ones with new keys at regular intervals (ip xfrm allows for that), but in practice for anything more complex it would be too much work for a task for which a well-known protocol exists, namely IKE, which is what should be used for any serious IPsec deployment in any case.