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 <–> <–IPsec–> <-> 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:

options IPSEC
device crypto

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 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

Now, time to setup the tunnels.

On A:

# ifconfig gre0 create
# ifconfig gre0 tunnel
# ifconfig gre0 inet netmask

On B is the same story, but in reverse:

# ifconfig gre0 create
# ifconfig gre0 tunnel # see note above about
# ifconfig gre0 inet netmask

If you played your cards right, you should now be able to, from host A:

% ping -c 10 
PING ( 56 data bytes
64 bytes from icmp_seq=0 ttl=63 time=10.093 ms
64 bytes from icmp_seq=1 ttl=63 time=8.499 ms
64 bytes from icmp_seq=2 ttl=63 time=12.634 ms
64 bytes from icmp_seq=3 ttl=63 time=11.859 ms
64 bytes from icmp_seq=4 ttl=63 time=10.973 ms
64 bytes from icmp_seq=5 ttl=63 time=10.034 ms
64 bytes from icmp_seq=6 ttl=63 time=8.861 ms
64 bytes from icmp_seq=7 ttl=63 time=12.768 ms
64 bytes from icmp_seq=8 ttl=63 time=11.829 ms
64 bytes from icmp_seq=9 ttl=63 time=10.840 ms

--- 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:


# These are the pre-shared keys. The SPIs (0x1000, 0x1001 are
# arbitrarily chosen).
# 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 esp 0x1000 -E 3des-cbc "insert random bytes here";
add esp 0x1001 -E 3des-cbc "insert different random bytes here";

# This is for IP compression; the SPIs here are also arbitrary.
add ipcomp 0x2000 -C deflate;
add ipcomp 0x2001 -C deflate;

# Route from to via the tunnel
# (A) to (B). The /require means that IPsec
# is required.
# Note, setkey/the kernel understand to mean "anywhere." If for some
# reason you want to secure all traffic over this tunnel, simply swap
# with here and below.
spdadd any -P out ipsec

# This is the reciprocal side from B to A.
spdadd any -P in ipsec

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 from host A, it will be transparently compressed, encrypted, and forwarded through (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).