Thursday, June 16, 2016

Cisco Routers: Easy Hair-Pin NAT for Internal Guest Network

Hey all! 

Recently I've been pouring myself into one particular configuration issue that is remarkably hard to solve on Cisco's IOS platform: Hairpin NAT. 

I've been tasked with designing a complete architecture for almost 20 sites. They want DMZs, Guest networks, lots of static and complex NATs, the works. And guess how many ASAs I can use? None

Which isn't that big of a deal, right? IOS has caught up to most security features over the past 5 or so years. NAT NVI (Nat Virtual Interface) can handle even complex NATs, ZBF (Zone-Based Firewall) is a nuanced and fantastic way to handle access control, etc. Hell, you can even build AnyConnect on IOS these days! 

But there's one problem that is intractable on Cisco's IOS platform: Hairpin-NAT

Because this problem is called so many things, let's define it. It's the issue that is seen when an internal host wants to access an internal server using the server's public (NAT'd) IP. Usually you don't see this with internal hosts, but the issue frequently comes up in designs when there is a "guest" network. Usually the guest network uses the same network infrastructure and internet connection as your other internal servers, but you use public DNS to make network segmentation clean and prevent as much internal access as possible. 
Picture from ServerFault
On an ASA, this problem is easy to solve - in fact, it's a single command.

same-security-traffic permit intra-interface

Surely it's this easy to solve on a router?

I tried to solve the problem in a half-dozen ways, and each has a "gotcha" that prevents it being useful in production. 

Potential Solution #1: Nat Virtual Interface (NVI) NAT'ing

This has the benefit of immediate results. Turn on NVI NAT'ing, modify the NAT statements and "ip nat enable" on a few interfaces, and you're good, all the guest network can use public IPs to access the DMZ servers. This technique is widely recommended as THE solution to this problem on the Internet. However, when there is more than an "inside" and "outside" interface, this type of NAT'ing gets very complex. 

As Alex points out in the comments below, you can make this work by creating exemptions in your NAT traffic selection. However, the complexity of this solution makes it less than ideal. 

Major drawback: Complexity with >2 interfaces and WAN NATing, high router utilization as NVI traffic is punted to the CPU for most ISR platforms, all NATs would have to be rewritten in the new style. 

Verdict: This is a no-go.

Potential Solution #2: Tons of Route-Maps

A widely cited solution from Tassos (who is a gosh-darn genius, and you should read his blog) nearly 8 years ago is to use regular NAT, but use route-maps to push traffic through an "ip nat outside" loopback address with policy routing on several interfaces. 

Major drawback: Super complex, high router utilization doing policy routing on several/all interfaces, I was not smart enough to get it working in a lab. 

Verdict: Not going to fly in a real production network (if I could even build it)

Potential Solution #3: Put the guest network on a separate internet connection

This is the solution often employed by Cisco shops that have become frustrated with the complexity and lack of manageability of Guest networks on the inside of the network. Shops either policy route the guest network out the other internet connection on their firewall or segment the network with a separate SSID/switch for guest that isn't connected to the internet network. 

Major drawback: As a consultant, I can't in good conscience ask the client to turn up 20 new consumer-class Internet circuits because I can't figure this issue out. 

VerdictDammit, I'm a good network engineer, I should be able to do this!

Potential Solution #4: Mix Up Some NATs

At this point I'm pulling my hair out, and really starting to believe that this just isn't possible, an answer that is widely circulated on the internet. I really just wish I could put the "guest" on the outside of my network. So I did. And dammit, it felt good. But of course, with both the "WAN" and "guest" in the same "ip nat outside" zone, there's no overload NAT to give the poor guys some Internets. 

And then I think of a funny joke. I wonder if I could use BOTH zoned NAT (ip nat inside/ip nat outside) AND stateless NAT (NVI) at the same time. I could just turn on stateless NAT on the guest and WAN zones, then write a single NAT statement to give it Internet access. I mean... there's no way Cisco would let you run both types of NAT at the same time, but I'm at wit's end. And low and behold, it flipping works. 

Major drawback: Configuration is slightly unorthodox. Using both NAT types looks funny, and someone might absent-mindedly 'fix' this in the future. Cisco might remove the older-style inside/outside NAT in future releases. 

VerdictSolution! (Short of Cisco actually supporting hairpin-nat, this'll do) 

Final Configuration

! Inside Host (vlan 1)
int eth 0/0
 ip add 192.168.1.50 255.255.255.0
ip route 0.0.0.0 0.0.0.0 192.168.1.1

! Guest Host (vlan 10)
int eth 0/0
 ip add 192.168.10.50 255.255.255.0
ip route 0.0.0.0 0.0.0.0 192.168.50.1

! DMZ Host (vlan 99)
int eth 0/0
 ip add 192.168.99.50 255.255.255.0
ip route 0.0.0.0 0.0.0.0 192.168.99.50

! Edge Router with router on a stick

! Interface configuration
interface Ethernet1/0.1
 encapsulation dot1Q 1 native
 ip address 192.168.1.1 255.255.255.0
 ip nat inside  <-- Most interfaces remain 'ip nat inside,' and NAT statements don't have to be rewritten

interface Ethernet1/0.10
 description GUEST
 encapsulation dot1Q 10
 ip address 192.168.10.1 255.255.255.0
 ip nat outside  <-- Note that 'guest' SVI is in outside zone. This enable all existing zoned-NATs to behave properly, without any config rewrites. So 'guest' users can access DMZ hosts using their public addresses just fine. 
 ip nat enable  <-- Stateless NAT also created on this interface

interface Ethernet1/0.99
 description DMZ
 encapsulation dot1Q 99
 ip address 192.168.99.1 255.255.255.0
 ip nat inside

interface Ethernet1/1
 description WAN
 ip address 20.0.0.2 255.255.255.0
 ip nat outside
 ip nat enable

! Route
ip route 0.0.0.0 0.0.0.0 20.0.0.1

! Object-groups and ACL

object-group network LocalGuest  <-- Highly recommend using object-groups to minimize configuration and simplify
 192.168.10.0 255.255.255.0
object-group network RFC1918Private 
 10.0.0.0 255.0.0.0
 172.16.0.0 255.240.0.0
 192.168.0.0 255.255.0.0

ip access-list extended Guest_2_WAN
 permit ip object-group LocalGuest any

ip access-list extended privateToPublic
 permit ip object-group RFC1918Private any

ip access-list extended siteToSite
 permit ip object-group RFC1918Private object-group RFC1918Private

route-map natOverload deny 10
 match ip address siteToSite
route-map natOverload permit 20
 match ip address privateToPublic

! NAT Configuration

ip nat inside source route-map natOverload interface Ethernet1/1 overload  <-- Regular inside/outside overload NAT for internet traffic

ip nat source list Guest_2_WAN interface Ethernet1/1 overload  <-- The new "NVI" style of NAT. Note the lack of "inside" in this command. Simply provides internet access to "guest" network segment. 

ip nat inside source static 192.168.99.50 20.0.0.50 extendable  <-- Hosts can still be exposed to the internet with regular "ip nat inside source static" commands. 

Lock it Down: ZBFW and Reflexive ACLs

I initially attempted to use ZBFW rules to lock down traffic from WAN-->Self and Self-->WAN, which works perfectly for zoned NAT (inside/outside). However, with NVI-style NAT, return internet traffic is evaluated by the edge router as destined for the edge router itself. Even when the outbound traffic is inspect on leaving this behavior occurs. 

But in engineering, there is always more than one way to skin that darn cat. So let's build some reflexive ACLs to permit return traffic. 

! Configuration: 


ip access-list extended In_2_Out
 permit ip any any reflect StatefulInbound  <-- Allow all traffic outbound, and remember it (default 300 seconds)

ip access-list extended Out_2_In
 evaluate StatefulInbound  <-- Allow all traffic that exited to return, using the stateful ACL table. 
 permit ip any host 20.0.0.50  <-- Permit inbound traffic for this particular DMZ services host


interface Ethernet1/1
 ip access-group Out_2_In in
 ip access-group In_2_Out out

In fact, you can still use ZBFW for most traffic, just make sure to permit it past the interface ACL so it can be picked up by NAT and then ZBF and delivered to internal hosts. 

Pro tip: If you're using this reflexive interface ACL as well as ZBF, remember your order of operations. The interface ACL is evaluated pre-NAT (so inbound traffic destinations will be public IPs), and ZBF is evaluted post-NAT (so inbound traffic dests will be your private IPs)

Download the GNS3 and Do It Yourself

There's nothing better for learning than building the thing yourself. Here's a completed GNS3 file with all features deployed. Please download and play with it yourself!

Files are here: 
https://1drv.ms/f/s!AliOPzHSO-GngrhpfKDb-YNfu1uLXQ

Good luck out there. 
Kyler