Saturday, November 12, 2011

Simple UDP ESP Encapsulation (NAT-T) for AWS EC2 IPSEC Tunnel

It is fairly straightforward to configure a simple AH/ESP IPSec tunnel between two endpoints, using the 'setkey' command, part of the ipsec-tools package on Debian or Ubuntu. (Useful resources are http://lartc.org/lartc.htmlhttp://ipsec-tools.sourceforge.net/checklist.html and http://www.ipsec-howto.org/x304.html).

Getting this to work on an AWS EC2 instance is currently impossible though, since AWS security groups do not route AH or ESP packets. They do however route UDP packets, so UDP-encapsulated ESP (otherwise known as NAT Traversal, or NAT-T) is an option. But documentation on how to enable this is sparse, to say the least, and in earlier versions of ipsec-tools, the man page does not even mention the existence of the esp-udp protocol.

Cutting to the chase, here is the setkey configuration required, which can be run on both endpoints.

#!/bin/bash
HOST1="host1.somewhere.net"
HOST2="host2.elsewhere.net"

if [ $(hostname) == "host1" ]; then
  IP_Local="$HOST1"
  IP_Remote="$HOST2"
else
  IP_Local="$HOST2"
  IP_Remote="$HOST1"
fi

cat <<_EOF_ | setkey -c
flush;
spdflush;

add $HOST1 $HOST2 esp-udp 0x100 -E 3des-cbc "123456789012123456789012";
add $HOST2 $HOST1 esp-udp 0x101 -E 3des-cbc "223456789012123456789012";

spdadd $IP_Local $IP_Remote any -P out ipsec
   esp/transport//require;

spdadd $IP_Remote $IP_Local any -P in ipsec
   esp/transport//require;
_EOF_


Running this, the only problem was (firewall rules aside) that the other side does not seem to process the UDP-encapsulated ESP packets received. tcpdump shows the ESP packets arriving at the other end of the link, but there they seem to meet with no response - they are not decapsulated!

# e.g.
# tcpdump -i eth1 -n 'host host1.somewhere.net and host host2.elsewhere.net and udp'
tcpdump: WARNING: eth1: no IPv4 address assigned
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes
18:25:29.214921 IP host1.4500 > host2.4500: UDP-encap: ESP(spi=0x00000100,seq=0x16), length 88

Eventually, after much Googling, I came across some references to this issue. It appears that the Linux kernel will not decapsulate the packets without some instruction. The strongSwan IPSec code (http://strongswan.sourcearchive.com/documentation/4.1.4/socket_8c-source.html), which contains a whole C function open_send_socket to arrange this, describes it thus:

/* We don't receive packets on the send socket, but we need a INBOUND policy.
* Otherwise, UDP decapsulation does not work!!! */
This begged the question, could the decapsulation be enabled, with a simple script? After much experimentation, it turns out the answer is yes.
#!/usr/bin/perl -w

use strict;

use Socket qw( IPPROTO_IP IPPROTO_UDP AF_INET SOCK_DGRAM SOL_SOCKET SO_REUSEADDR INADDR_ANY sockaddr_in );

my $UDP_ENCAP = 100;

# UDP encapsulation types
my $UDP_ENCAP_ESPINUDP_NON_IKE = 1; # /* draft-ietf-ipsec-nat-t-ike-00/01 */
my $UDP_ENCAP_ESPINUDP = 2; # /* draft-ietf-ipsec-udp-encaps-06 */
my $UDP_ENCAP_L2TPINUDP = 3; # /* rfc2661 */

my $Sock;

socket( $Sock, AF_INET, SOCK_DGRAM, IPPROTO_UDP ) || die "::socket: $!";
setsockopt( $Sock, SOL_SOCKET, SO_REUSEADDR, pack( "l", 1 ) );

# struct sadb_x_policy {
# uint16_t sadb_x_policy_len;
# uint16_t sadb_x_policy_exttype;
# uint16_t sadb_x_policy_type;
# uint8_t sadb_x_policy_dir;
# uint8_t sadb_x_policy_reserved;
# uint32_t sadb_x_policy_id;
# uint32_t sadb_x_policy_priority;
# } __attribute__((packed));
# /* sizeof(struct sadb_x_policy) == 16 */

my $SADB_X_EXT_POLICY = 18;
my $IP_IPSEC_POLICY = 16;
my $IPSEC_POLICY_BYPASS = 4;
my $IPSEC_DIR_INBOUND = 1;
my $IPSEC_DIR_OUTBOUND = 2;

# policy.sadb_x_policy_len = sizeof(policy) / sizeof(u_int64_t);
# policy.sadb_x_policy_exttype = SADB_X_EXT_POLICY;
# policy.sadb_x_policy_type = IPSEC_POLICY_BYPASS;
# policy.sadb_x_policy_dir = IPSEC_DIR_OUTBOUND;

my $policy1 = pack("SSSCCLL", 2, $SADB_X_EXT_POLICY, $IPSEC_POLICY_BYPASS, $IPSEC_DIR_OUTBOUND, 0, 0, 0);
my $policy2 = pack("SSSCCLL", 2, $SADB_X_EXT_POLICY, $IPSEC_POLICY_BYPASS, $IPSEC_DIR_INBOUND, 0, 0, 0);

# See http://strongswan.sourcearchive.com/documentation/4.1.4/socket_8c-source.html
if( defined setsockopt( $Sock, IPPROTO_IP, $IP_IPSEC_POLICY, $policy1 ) ) {
   print "setsockopt:: policy OK\n"; }
else { print "setsockopt:: policy FAIL\n"; }

if( defined setsockopt( $Sock, IPPROTO_IP, $IP_IPSEC_POLICY, $policy2 ) ) {
   print "setsockopt:: policy OK\n"; }
else { print "setsockopt:: policy FAIL\n"; }

if( defined setsockopt( $Sock, IPPROTO_UDP, $UDP_ENCAP, $UDP_ENCAP_ESPINUDP) ) {
   print "setsockopt:: UDP_ENCAP OK\n"; }
else { print "setsockopt:: UDP_ENCAP FAIL\n"; }

bind( $Sock, sockaddr_in( 4500, INADDR_ANY ) ) || die "::bind: $!";

sleep;

1;

The sleep command at the end is important: as soon as this script is terminated, the UDP decapsulation ends. However, while it runs, it works just fine. The result is that, once incoming UDP traffic on port 4500 is enabled in the relevant EC2 Security Group, you can now set up a simple NAT-T IPSec VPN, using the above two scripts!

P.S. Don't forget to generate your own encryption keys - the ones above are obviously insecure!