As part of another OpenBSD & IPsec problem I’m investigating, I was pointed at RFC 3884 which puts forward a solution for solving problems with dynamic routing protocols and the use of IPsec in tunnel mode.
The RFC covers a few scenarios and solutions, but the main solution put forward is to replace IPsec tunnel mode with a combination of IPsec transport mode and the use of IP-in-IP tunnels. This has the benefit that it remains interoperable with IPsec tunnel mode implementations as the packet format is identical.
To show this you need to understand how IPsec packets are constructed. The RFC covers this and there also sites like this one which give you better diagrams. You only really need to pay attention to how ESP works rather than AH as AH is rarely used due to it’s shortcomings with the dreaded NAT. Once you know that all an IP-in-IP tunnel does is encapsulate an existing IP packet with another one you can see how the two implementations have the same wire format, the innermost packets just have a slightly different route through the network stack.
Anyway, after reading this I thought I’d test it to see if it really does work between a pair of OpenBSD hosts so I fired up a couple of stock 4.7 installs under VMware.
On both hosts I disabled pf to eliminate that as a source of any failures:
For reasons I’ll explain later, we’ll just deal with manually keyed IPsec so you’ll need to generate a handful of keys. On each host generate an authentication and encryption key:
# openssl rand 32 | hexdump -e '"0x" 32/1 "%02x" "\n"'
# openssl rand 16 | hexdump -e '"0x" 16/1 "%02x" "\n"'
On the first host we’ll set up the regular IPsec tunnel. Add the following to
/etc/ipsec.conf using the keys we generated:
flow esp from 10.0.0.1/32 to 10.0.0.2/32 local 172.16.252.128 peer 172.16.252.130
esp tunnel from 172.16.252.128 to 172.16.252.130 spi 0xdeadbeef:0xfeedbeef \
authkey 0xebfd3e8038fd1c9eadcc32308e5f12aba813114614855e4a93e7407f3c40f827:0x7a5a5832c43395c62aa1c443cfda68f3f8fff89378fdb2df40419df01547a40a \
We’ll need a dummy interface with the 10.0.0.1 address for testing, the easiest way is to just use the loopback interface like so:
# ifconfig lo1 create
# ifconfig lo1 10.0.0.1 prefixlen 32
On the second host, we’ll need to set up the IP-in-IP tunnel which on an OpenBSD host uses the
# ifconfig gif0 create
# ifconfig gif0 tunnel 172.16.252.130 172.16.252.128
# ifconfig gif0 10.0.0.2 10.0.0.1 prefixlen 32
Now add the following to
/etc/ipsec.conf using the same keys, but remember to swap around the various parameters:
flow esp proto ipencap from 172.16.252.130 to 172.16.252.128
esp transport from 172.16.252.130 to 172.16.252.128 spi:0xfeedbeef:0xdeadbeef \
authkey 0x7a5a5832c43395c62aa1c443cfda68f3f8fff89378fdb2df40419df01547a40a:0xebfd3e8038fd1c9eadcc32308e5f12aba813114614855e4a93e7407f3c40f827 \
Note that we only apply transport mode to the IP-in-IP tunnel packets (proto ipencap) otherwise all other traffic will be dropped by the first host if you tried ping, SSH, etc. between them.
With that done, on each host now do:
# ipsecctl -f /etc/ipsec.conf
Everything should be working now so on the first host with the regular IPsec tunnel, try pinging the remote IP address. You’ll need to specify the source address so it matches the IPsec flow:
# ping -I 10.0.0.1 10.0.0.2
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: icmp_seq=0 ttl=255 time=0.628 ms
64 bytes from 10.0.0.2: icmp_seq=1 ttl=255 time=0.670 ms
While that’s running, on the opposite host you should be able to use tcpdump to see the IPsec traffic flowing back and forth:
# tcpdump -nn -i vic0
tcpdump: listening on vic0, link-type EN10MB
23:43:43.067641 esp 172.16.252.128 > 172.16.252.130 spi 0xdeadbeef seq 23 len 136
23:43:43.067880 esp 172.16.252.130 > 172.16.252.128 spi 0xfeedbeef seq 55 len 136
You can also use tcpdump on the
enc0 interface to see the packets passing through the IPsec stack:
# tcpdump -nn -i enc0
tcpdump: listening on enc0, link-type ENC
23:46:51.679546 (authentic,confidential): SPI 0xdeadbeef: 10.0.0.1 > 10.0.0.2: icmp: echo request (encap)
23:46:51.679628 (authentic,confidential): SPI 0xfeedbeef: 10.0.0.2 > 10.0.0.1: icmp: echo reply (encap)
Here you can see the actual payload of the packets, in this case ICMP.
You can repeat the test from the opposite side with the
# ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: icmp_seq=0 ttl=255 time=1.008 ms
64 bytes from 10.0.0.1: icmp_seq=1 ttl=255 time=0.549 ms
Note that you don’t need to specify a source address this time as the routing sorts this out and correctly assigns you the local address of the IP-in-IP tunnel interface, (this is in fact another perceived benefit of the solution proposed by the RFC).
So we’ve hopefully proved it works, however this has only been demonstrated for manually keyed IPsec associations. Most IPsec these days is set up using IKE to automatically exchange keys in various forms and dynamically configure the flows on the hosts.
Here lies the problem, if I set up each end to use
isakmpd(8) neither end will agree on a proposal as one side will propose tunnel mode and the other will propose transport mode. What I need to be able to do is get the transport side to propose tunnel mode but actually configure transport mode flows on the local side and rely on a suitable
gif(4) interface to be set up. Currently there doesn’t appear to be a way to do this with