Conditional Routing with PF

Public IPs have been replaced with TEST-NET equivalents for obvious reasons. Enjoy!

I have a rather nice ISP which allows me to use two public IPv4 addresses: one is dynamic and routed through their gateway at 192.168.64.1 and the other one is static, bridged directly to the internet through an extra port on the modem. On the interface connected there I have to configure the provided IP address (203.0.113.52/24) and the default gateway of 203.0.113.52.

Nobody’s perfect, so locking yourself out with a bad pf rule isn’t so much a possibility but more an eventuality. Thankfully, the modem itself is also capable of performing NAT on the dynamic IP, so a backup connection is also available, right?

Well, no. Trying to connect through modem NAT times out. A bit of tcpdump clearly shows the packets do arrive at my firewall but due to the way NAT works, my firewall sends its replies to the external static IP gateway instead of the modem.

Why did this happen?

Port forwarding or Destination NAT – DNAT works by – you guessed it – overwriting the destination address and optionally port of receieved packages. This means that a packet arriving at the modem gets translated like this:

Packet arriving at:SourceDestination
Modem’s NAT ruleany:9013192.168.64.13:22
Modem192.0.2.5:45533198.51.100.43:9013 (my modem)
Firewall192.0.2.5:45533192.168.64.13:22
DescriptionSourceDestination
What client expects198.51.100.43:9013192.0.2.5:45533
What client gets/firewall sends203.0.113.52:22192.0.2.5:45533

Modem establishes a NAT state in it’s firewall table, expecting the firewall to reply back to it. Unfortunately, because the source address remains the same, the firewall sees the external source address (192.0.2.5:45533) and sends the reply via its own default gateway, 203.0.113.1. The packets do arrive at the origin (192.0.2.5:45533), but because the original client expects replies from 198.51.100.43:9013 instead of 203.0.113.52:22, it doesn’t recognize the packets as belonging to the connection and drops them.

How do we solve this?

There’s more than one way to skin a cat. The boring way would be to make the modem perform both S- and D-NAT, which would ensure the packets travel properly. As my ISP doesn’t let customers to configure their modems, a different, more interesting solution must surely exist.

What about tools already available? As always, pf has something suited just for this purpose. Reading pf.conf(5) tells us this:

Symmetric routing enforcement sound like something I’d need. And indeed, writing a rule like:

pass in on ingress from !(ingress:network) to any port ssh reply-to $router-ip@ingress

Fixes that problem, sending the NAT’d SSH packets from my modem back the same path they arrived while not breaking existing routing configuration.

Leave a Reply

Your email address will not be published. Required fields are marked *