Setting up an IPsec VPN between two FreeBSD hosts
I spent a fairly long amount of time trying to figure out how to setup IPsec between to FreeBSD hosts on the internet, with the express goal of being able to route generic traffic over the VPN. This proved to be an interesting trial-and-error process where I learned a fair bit about the difference between “tunnel” and “transport” modes; but eventually I got it to work.
For my purposes, this setup is what I desired:
.—————-. .————-. .———.
| 192.168.0.x/24 <–> 192.168.0.1 <–IPsec–> 184.108.40.206 <-> Net
`—————-‘ `————-‘ `———‘
Home Network VPN endpoint A VPN endpoint B
I wanted all traffic on the left of A (my home network) to be able to access the “Net” on the right side of B. Because the “Net” on the right side of B won’t route traffic back to my RFC1918 addressing, VPN endpoint B is doing NAT translation. Both A and B are FreeBSD boxes.
So, first off, you’ll need to rebuild your kernels. FreeBSD does not include IPsec in the base kernel because of a performance hit (PR conf/128030). This is documented in the FreeBSD handbook entry on IPsec. In a nutshell though, fork the GENERIC config and add these lines to it:
Then make buildkernel, make installkernel, and reboot (note: installing a custom kernel has some downsides, namely in the freebsd-update(8) department).
Now, here comes the fun part.
In front of endpoint A, I have a NAT router of my own between A and the internet. It has a handy feature for forwarding GRE packets to a nominated destination host internally; so for this example, I’m using GRE as the tunneling protocol. For the tunnel, you need to number them with their own IP addresses. So I have now:
|Host||Network||em0 IPv4||gre0 IPv4|
(note: em0 just happens to exist on both boxes, you probably have a different interface name).
Important note: the NAT router for host A has a public IP address of 220.127.116.11. This is important for setting up B’s tunnel below. In effect, you need to set the GRE end of the tunnel to the NAT router’s external address, which will then transparently forward those packets to 192.168.0.1.
Now, time to setup the tunnels.
# ifconfig gre0 create # ifconfig gre0 tunnel 192.168.0.1 18.104.22.168 # ifconfig gre0 inet 172.16.0.1 172.16.0.2 netmask 255.255.255.255
On B is the same story, but in reverse:
# ifconfig gre0 create # ifconfig gre0 tunnel 22.214.171.124 126.96.36.199 # see note above about 188.8.131.52. # ifconfig gre0 inet 172.16.0.2 172.16.0.1 netmask 255.255.255.255
If you played your cards right, you should now be able to, from host A:
% ping -c 10 172.16.0.2 PING 172.16.0.2 (172.16.0.2): 56 data bytes 64 bytes from 172.16.0.2: icmp_seq=0 ttl=63 time=10.093 ms 64 bytes from 172.16.0.2: icmp_seq=1 ttl=63 time=8.499 ms 64 bytes from 172.16.0.2: icmp_seq=2 ttl=63 time=12.634 ms 64 bytes from 172.16.0.2: icmp_seq=3 ttl=63 time=11.859 ms 64 bytes from 172.16.0.2: icmp_seq=4 ttl=63 time=10.973 ms 64 bytes from 172.16.0.2: icmp_seq=5 ttl=63 time=10.034 ms 64 bytes from 172.16.0.2: icmp_seq=6 ttl=63 time=8.861 ms 64 bytes from 172.16.0.2: icmp_seq=7 ttl=63 time=12.768 ms 64 bytes from 172.16.0.2: icmp_seq=8 ttl=63 time=11.829 ms 64 bytes from 172.16.0.2: icmp_seq=9 ttl=63 time=10.840 ms --- 172.16.0.2 ping statistics --- 10 packets transmitted, 10 packets received, 0.0% packet loss round-trip min/avg/max/stddev = 8.499/10.839/12.768/1.399 ms
Now, time to secure stuff.
I fuddled around with racoon a bit, but then realized a much simpler solution would be to simply use pre-shared keys for this case. The neat thing about IPsec is its in-kernel policy engine. It’s not unlike a routing table in the sense that you can say: “for traffic to Y, encrypt like this. for traffic from Y, decrypt like this.” By putting this in kernel-land, you can, in effect secure transmission of data between two hosts at the kernel layer and have your applications blissfully unaware of what’s happening.
On FreeBSD (and I imagine anything with KAME IPsec), this kernel policy engine is managed with a userland tool called setkey(8).
For this use, I ended up with this basic configuration:
flush; spdflush; # These are the pre-shared keys. The SPIs (0x1000, 0x1001 are # arbitrarily chosen). # www.itkylin.com # Feel free to pick your own encryption algorithm here. I chose # 3des-cbc based on another example I followed. # # I grabbed random bytes by: head -1 /dev/random | uuencode - | head -2 | tail -1 add 172.16.0.2 172.16.0.1 esp 0x1000 -E 3des-cbc "insert random bytes here"; add 172.16.0.1 172.16.0.2 esp 0x1001 -E 3des-cbc "insert different random bytes here"; # This is for IP compression; the SPIs here are also arbitrary. add 172.16.0.2 172.16.0.1 ipcomp 0x2000 -C deflate; add 172.16.0.1 172.16.0.2 ipcomp 0x2001 -C deflate; # Route from 192.168.0.0/24 to 184.108.40.206/8 via the tunnel # 172.16.0.1 (A) to 172.16.0.2 (B). The /require means that IPsec # is required. # www.itkylin.com # Note, setkey/the kernel understand 0.0.0.0/0 to mean "anywhere." If for some # reason you want to secure all traffic over this tunnel, simply swap # 220.127.116.11/8 with 0.0.0.0/0 here and below. spdadd 192.168.0.0/24 18.104.22.168/8 any -P out ipsec ipcomp/tunnel/172.16.0.1-172.16.0.2/require esp/tunnel/172.16.0.1-172.16.0.2/require; # This is the reciprocal side from B to A. spdadd 22.214.171.124/8 192.168.0.0/24 any -P in ipsec ipcomp/tunnel/172.16.0.2-172.16.0.1/require esp/tunnel/172.16.0.2-172.16.0.1/require;
This file is good on both hosts A and B. You can load it with: setkey -f setkey.conf
When you load these rules in, it actually has the same effect as a routing table change. You’ll find that when you want to talk to anything in 126.96.36.199/8 from host A, it will be transparently compressed, encrypted, and forwarded through 172.16.0.2 (host B) and from then onwards. Make sure you turn on IP forwarding (net.inet.ip.forwarding) and NAT (if appropriate) on both hosts A and B.
This solution worked out rather well for me. I benchmarked my IPsec tunnel at around 8mbps (1000 kbytes/sec) on a dual-core Pentium D and a 30mbit cable connection (host B is on 100mbit), but this was unscientific and could have been limited by numerous factors. Suffice it to say, it went as fast as I wanted it to, and aside from a bit of processor load during high traffic, had negligible overhead. To ensure everything was working, I packet-captured from both ends to look for ESP packets with the correct SPI (in this case 0x1000 or 0x1001 based on direction of traffic).