Saturday, March 22, 2014

ASA Hairpinning and TCP state bypass

So what is hairpinning, anyway?  Hairpinning is when traffic received on an interface is immediately routed back out the same interface.  If you visualize the packet flow, it looks something like a hairpin:

The command "same-security-traffic permit intra-interface" allows us to hairpin traffic on an ASA.  The most common use case is allowing remote access VPN traffic to traverse a site to site VPN tunnel as shown in the diagram above.

However, since we have the ability to hairpin VPN traffic, it seems safe to assume that we can hairpin other traffic as well.

Let’s look at the following scenario:



The device 192.168.1.10 is using the ASA as its default gateway.  It needs to send traffic to 192.168.2.10.

First of all, there may be better ways to accomplish this.  We could use use the L3 switch as the default gateway for all devices and let it handle our routing.  We could also create an interface on the ASA on the 192.168.2.0/24 network, provided we have a Security Plus license.

However, in certain circumstances neither of these solutions may be viable options.  So how do we make this work with the topology above?  In theory, we could simply create a static route on the ASA to pass traffic to the next hop of the other L3 device:

asa01(config)# route inside 192.168.2.0 255.255.255.0 192.168.1.254

However, if we attempt to pass traffic before applying the same-security-traffic permit intra-interface command, we will receive the following output when running packet-tracer:
asa01# packet-tracer input inside tcp 192.168.1.10 55555 192.168.2.10 3389 detail

<output cut>

Result:
input-interface: inside
input-status: up
input-line-status: up
output-interface: inside
output-status: up
output-line-status: up
Action: drop
Drop-reason: (acl-drop) Flow is denied by configured rule

asa01#

Traffic is being stopped by a configured rule – by default, the ASA will not route traffic between same-security interfaces or out the same interface through which it was received.  If we apply the same-security-traffic permit intra-interface command and run packet-tracer again, we will see the following:
asa01(config)# same-security-traffic permit intra-interface
asa01(config)# packet-tracer input inside tcp 192.168.1.10 55555 192.168.2.10 3389 detail

<output cut>

Result:
input-interface: inside
input-status: up
input-line-status: up
output-interface: inside
output-status: up
output-line-status: up
Action: allow

asa01(config)#

So the ASA is willing to pass the traffic, provided we tell it to permit intra-interface traffic.  After applying this command, if we ping from 192.168.1.10 to 192.168.2.10 we will see replies.  However, other traffic will not appear to be routed properly.  When attempting to initiate an RDP session from 192.168.1.10 to 192.168.2.10, a packet capture on 192.168.2.10 shows the following:


As we see in the capture, 192.168.2.10 is receiving the traffic from 192.168.1.10.  However, 192.168.1.10 is not receiving the reply traffic – it is being dropped by the ASA, and a TCP session cannot be established.

We should see:
192.168.1.10 -> 192.168.2.10 SYN
192.168.2.10 -> 192.168.1.10 SYN-ACK
192.168.1.10 -> 192.168.2.10 ACK

However, we see:
192.168.1.10 -> 192.168.2.10 SYN
192.168.2.10 -> 192.168.1.10 SYN-ACK
192.168.1.10 -> 192.168.2.10 RST

On a router, this topology would work without any additional configuration.  So why doesn't it work on an ASA?  The root of our problem is how the ASA handles asymmetric routing.  In our topology, the packet flow is as follows:
outbound: 192.168.1.10 -> ASA -> L3 Switch -> 192.168.2.10
inbound: 192.168.2.10 -> L3 Switch -> 192.168.1.10

Because of the stateful packet inspection occurring on the ASA, all TCP sessions are tracked in the connection table.  Due to the asymmetric nature of this topology, the ASA will not recognize return traffic as part of the same TCP session and a connection will never be established.  See below:
asa01# sh conn | inc 192.168
asa01#

If we had a router in place of the ASA at 192.168.1.1, one of two things would happen:

  1. Asymmetric routing would simply be allowed because SPI would not be performed.
  2. Instead of asymmetric routing, the router would issue an ICMP redirect.   This means the router would send an ICMP message telling the workstation at 192.168.1.10 to put a static route in its routing table for 192.168.2.10:
IPv4 Route Table
===========================================================================
Active Routes:
Network Destination        Netmask          Gateway       Interface  Metric
     192.168.2.10  255.255.255.255    192.168.1.254    192.168.1.10      11

In this case, traffic would be sent directly to 192.168.1.254 and not hairpinned on the inside interface.

The packet flow would be symmetric after the redirect:
outbound: 192.168.1.10 -> L3 Switch -> 192.168.2.10
inbound: 192.168.2.10 -> L3 Switch -> 192.168.1.10

Unfortunately, neither of these options work on the ASA.  ASAs do not support ICMP redirects, and their stateful packet inspection breaks asymmetric routing of TCP sessions.  So what is the solution?

Back in ASA software version 8.2, Cisco implemented a feature called TCP state bypass.  TCP state bypass does exactly what the name says: bypasses the stateful inspection of TCP sessions for traffic that we explicitly define.

TCP state bypass configuration is relatively straightforward.
  1. Create an access-list to define the traffic we want to exempt from tcp state inspection.
  2. Create a class-map and match this access-list.
  3. Create a policy-map and apply the class we created.
  4. Configure tcp-state-bypass on the policy map.
  5. Apply the policy to the appropriate interface.
See the configuration below:
asa01(config)# access-list tcp_bypass extended permit tcp 192.168.1.0 255.255.255.0 192.168.2.0 255.255.255.0
asa01(config)# class-map tcp_bypass
asa01(config-cmap)# match access-list tcp_bypass
asa01(config-cmap)# policy-map tcp_bypass_policy
asa01(config-pmap)# class tcp_bypass
asa01(config-pmap-c)# set connection advanced-options tcp-state-bypass
asa01(config-pmap-c)# service-policy tcp_bypass_policy interface inside
asa01(config)#

After applying this configuration, if we attempt to RDP into 192.168.2.10 from 192.168.1.10 again, the connection is successful.  We are now able to successfully hairpin TCP traffic on the inside interface of the ASA!  We can see the TCP connection established between 192.168.2.10 and 192.168.1.10, both on the inside interface.
asa01# sh conn | inc 192.168
TCP inside  192.168.2.10:3389 inside  192.168.1.10:53321, idle 0:00:02, bytes 19, flags b
asa01#

As the above example illustrates, the ASA can route traffic, but is most certainly not a router.  There are fundamental differences with the behavior of an ASA that can make troubleshooting, well, troublesome, until these differences are understood.

Here are additional resources that I found helpful:
http://www.cisco.com/en/US/products/ps6120/products_configuration_example09186a0080b2d922.shtml
https://supportforums.cisco.com/docs/DOC-17810
http://www.cisco.com/en/US/docs/security/asa/asa82/configuration/guide/conns_tcpstatebypass.pdf
http://nat0.net/cisco-asa-hairpinning/

4 comments:

  1. This was exactly what was needed - thanks for the solid writeup.

    ReplyDelete
  2. Hi, what do you have to do to make it work for UDP?

    ReplyDelete
  3. Hi,
    It does not work for ICMP traffic, does it ?

    ReplyDelete