2024-10-23
Doing basic policy based routing on FreeBSD with PF rules
Suppose, not hypothetically, that you have a FreeBSD machine that has two interfaces and these two interfaces are reached through different firewalls. You would like to ping both of the interfaces from your monitoring server because both of them matter for the machine's proper operation, but to make this work you need replies to your pings to be routed out the right interface on the FreeBSD machine. This is broadly known as policy based routing and is often complicated to set up. Fortunately FreeBSD's version of PF supports a basic version of this, although it's not well explained in the FreeBSD pf.conf manual page.
To make our FreeBSD machine reply properly to our monitoring machine's
ICMP pings, or in general to its traffic, we need a stateful 'pass' rule
with a 'reply-to
':
B_IF="emX" B_IP="10.x.x.x" B_GW="10.x.x.254" B_SUBNET="10.x.x.0/24" pass in quick on $B_IF \ reply-to ($B_IF $B_GW) \ inet from ! $B_SUBNET to $B_IP \ keep state
(Here $B_IP
is the machine's IP on this second interface, and
we also need the second interface, the gateway for the second
interface's subnet, and the subnet itself.)
As I discovered,
you must put the 'reply-to
' where it is here, although as far as
I can tell the FreeBSD pf.conf manual page will only tell you
that if you read the full BNF. If you put it at the end the way
you might read the text description, you will get only opaque
syntax errors.
We must specifically exclude traffic from the subnet itself to us,
because otherwise this rule will faithfully send replies to other
machines on the same subnet off to the gateway, which either won't
work well or won't work at all. You can restrict the PF rule more
narrowly, for example 'from { IP1 IP2 IP3 }
' if those are the
only off-subnet IPs that are supposed to be talking to your secondary
interface.
(You may also want to match only some ports here, unless you want
to give all incoming traffic on that interface the ability to talk
to everything on the machine. This may require several versions of
this rule, basically sticking the 'reply-to ...
' bit into every
'pass in quick on ...' rule you have for that interface.)
This PF rule only handles incoming connections (including implicit ones from ICMP and UDP traffic). If we want to be able to route our outgoing traffic over our secondary interface by selecting a source address when you do things, we need a second PF rule:
pass out quick \ route-to ($B_IF $B_GW) \ inet from $B_IP to ! $B_SUBNET \ keep state
Again we must specifically exclude traffic to our local network,
because otherwise it will go flying off to our gateway, and also
you can be more specific if you only want this machine to be able
to connect to certain things using this gateway and firewall (eg
'to { IP1 IP2 SUBNET3/24 }
', or you could use a port-based
restriction).
(The PF rule can't be qualified with 'on $B_IF
', because the
situation where you need this rule is where the packet would not
normally be going out that interface. Using 'on <the interface with
your default route's gateway>' has some subtle differences in the
semantics if you have more than two interfaces.)
Although you might innocently think otherwise, the second rule by
itself isn't sufficient to make incoming connections to the second
interface work correctly. If you want both incoming and outgoing
connections to work, you need both rules. Possibly it would work
if you matched incoming traffic on $B_IF
without keeping state.