PF Issues with pf.conf

Hi all

I had a working pf.conf configuration with a block log all, but I couldn't get port forwarding to work with that rule in place.
I need at least a block rule for incoming traffic on the WAN interface, but now my port forwarding works, though all internal networks can also communicate with each other.
What I’m aiming for is a secure FreeBSD router/firewall setup with a WAN interface, where I can configure port forwarding, and three internal networks that can only access the internet. For this, I’ve written the following rule:
Code:
pass in log on $LAN_IF inet proto { udp, tcp } from $LAN_NET to ! <RFC1918> keep state
I intended to later specify traffic between the subnets.
I’m also really keen on blocking traffic based on the source address, and that worked until I tried to get my subnet-to-subnet rules functioning. The pass quick on rule works fine, like this:
Code:
pass quick on $SERVER_IF proto tcp from $LAN_NET to 10.0.20.20 port 22
However, I would prefer to block by "source" rather than using the pass quick on approach.
I’d really appreciate any help in achieving a more secure configuration. I want to start with a block all rule at the top, but still allow the necessary traffic to work.
Currently, after changing my block all to block in log on $WAN_IF, I find that my three internal networks are completely open to each other, which I want to avoid.

Any advice or suggestions would be greatly appreciated!

Code:
# Interfaces
WAN_IF = "vtnet0"        # external interface (Internet-facing)
LAN_IF = "vtnet1"     # Office network
SERVER_IF = "vtnet2"     # Server network
VIDEO_IF  = "vtnet3"      # Guest network
INT_IFS = "{ LAN_IF, SERVER_IF, VIDEO_IF }"

# Subnets
LAN_NET = "10.0.10.0/24"
SERVER_NET = "10.0.20.0/24"
VIDEO_NET = "10.0.30.0/24"

# RFC1918 (Private networks)
#RFC1918 = "{ 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }"
table <RFC1918> const { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }

# Global Settings and Optimizations
set block-policy return         # Return RST for blocked TCP, ICMP unreachable for other protocols
set skip on lo0                 # Skip filtering on the loopback interface
set optimization normal         # Optimize for normal network conditions
set require-order yes           # Process rules in the order they appear
set state-policy if-bound       # Tie states to the interface they originate from
set loginterface $WAN_IF        # Log all traffic on the WAN interface for debugging

# Scrub packets (normalize traffic to prevent fragmentation-based attacks)
scrub in on $WAN_IF all

# NAT Configuration
nat on $WAN_IF from <RFC1918> to any -> ($WAN_IF)
rdr on $WAN_IF inet proto tcp from any to ($WAN_IF) port 80 -> 10.0.20.12 port 80
rdr on $WAN_IF inet proto tcp from any to ($WAN_IF) port 443 -> 10.0.20.12 port 443

# Default Block WAN
# block log all # This blocked rdr
block in log on $WAN_IF

# Block traffic to blocked IPs
block in log quick on $INT_IFS inet from <RFC1918> to <blocked_ips>

# Allow Ping
pass inet proto icmp icmp-type echoreq

# Allow outbound traffic from the firewall itself
pass out on $WAN_IF inet from ($WAN_IF) to any keep state

# Allow firewall to communicate with internal networks
pass out on $INT_IFS inet from self to <RFC1918> keep state
pass out on $LAN_IF inet from self to $LAN_NET keep state
pass out on $SERVER_IF inet from self to $SERVER_NET keep state

# Allow internal networks to access the Internet but block RFC1918 destinations
pass in log on $LAN_IF inet proto { udp, tcp } from $LAN_NET to ! <RFC1918> keep state
pass in log on $SERVER_IF inet proto { udp, tcp } from $SERVER_NET to ! <RFC1918> keep state

# Allow internal networks to access services on the firewall (DHCP, DNS, NTP)
pass in on $SERVER_IF inet proto { udp, tcp } from $SERVER_NET to self port { 22, 53, 67, 68, 123 } keep state
pass in on $LAN_IF inet proto { udp, tcp } from $LAN_NET to self port { 22, 53, 67, 68, 123 } keep state
pass in on $VIDEO_IF inet proto { udp, tcp } from $VIDEO_NET to self port { 53, 67, 68, 123 } keep state

# Internal rules
pass quick on $SERVER_IF proto tcp from $LAN_NET to 10.0.20.20 port { 22 }

# Port Forwarding Rules
pass in log on $WAN_IF inet proto tcp from any to 10.0.20.12 port 80 keep state
pass in log on $WAN_IF inet proto tcp from any to 10.0.20.12 port 443 keep state

# Firewall rules
pass in log on $WAN_IF inet proto tcp from any to ($WAN_IF) port 8123 keep state
 
Code:
INT_IFS = "{ LAN_IF, SERVER_IF, VIDEO_IF }"
That should probably be INT_IFS="{" $LAN_IF, $SERVER_IF, $VIDEO_IF "}"
Code:
MACROS
     Macros can be defined that will later be expanded in context.  Macro
     names must start with a letter, and may contain letters, digits and
     underscores.  Macro names may not be reserved words (for example pass,
     in, out).  Macros are not expanded inside quotes.

     For example,

           ext_if = "kue0"
           all_ifs = "{" $ext_if lo0 "}"
           pass out on $ext_if from any to any
           pass in  on $ext_if proto tcp from any to any port 25

Code:
nat on $WAN_IF from <RFC1918> to any -> ($WAN_IF)
You wanted security, yet you're NAT'ing for a whole bunch of source addresses that aren't even part of your network. Don't do that.

Code:
# Subnets
LAN_NET = "10.0.10.0/24"
SERVER_NET = "10.0.20.0/24"
VIDEO_NET = "10.0.30.0/24"
You can replace these with $LAN_IF:network for example. Or simpler assign those interface to a "lan" interface group and use :network on the interface group.
Code:
           Interface names and interface group names can have modifiers
           appended:

           :network      Translates to the network(s) attached to the
                         interface.
           :broadcast    Translates to the interface's broadcast address(es).
           :peer         Translates to the point-to-point interface's peer
                         address(es).
           :0            Do not include interface aliases.

Code:
# Block traffic to blocked IPs
block in log quick on $INT_IFS inet from <RFC1918> to <blocked_ips>
The blocked_ips table doesn't exist.

Code:
# Allow internal networks to access services on the firewall (DHCP, DNS, NTP)
pass in on $SERVER_IF inet proto { udp, tcp } from $SERVER_NET to self port { 22, 53, 67, 68, 123 } keep state
pass in on $LAN_IF inet proto { udp, tcp } from $LAN_NET to self port { 22, 53, 67, 68, 123 } keep state
pass in on $VIDEO_IF inet proto { udp, tcp } from $VIDEO_NET to self port { 53, 67, 68, 123 } keep state
SSH only uses TCP/22. DNS can be UDP or TCP on 53.
DHCP (67) is UDP only, the server is on port 67, the client listens on 68. And you also need to account for the initial DHCPDISCOVER being sent to the broadcast address (255.255.255.255).
NTP (123) is UDP only. https://en.wikipedia.org/wiki/Network_Time_Protocol

Code:
# Internal rules
pass quick on $SERVER_IF proto tcp from $LAN_NET to 10.0.20.20 port { 22 }
Traffic from $LAN_NET to 10.0.20.20 comes in on $LAN_IF and goes out of $SERVER_IF. If you have a block all this would need two pass rules, one for the traffic coming in on $LAN_IF and one for the traffic going out of $SERVER_IF.

Code:
# Port Forwarding Rules
pass in log on $WAN_IF inet proto tcp from any to 10.0.20.12 port 80 keep state
pass in log on $WAN_IF inet proto tcp from any to 10.0.20.12 port 443 keep state
These look good you avoided a common pitfall (NAT/redirection happens before the rules are evaluated), but again, if you have a block all you will also need to pass the traffic going out of $SERVER_IF (because that's where 10.0.20.12 is).
 
You could add 'pass' to your rdr rules, then the redirected traffic will skip subsequent filtering rules:

rdr pass on $WAN_IF inet proto tcp from any to ($WAN_IF) port 80 -> 10.0.20.12 port 80 rdr on $WAN_IF inet proto tcp from any to ($WAN_IF) port 443 -> 10.0.20.12 port 443
rdr pass on $WAN_IF inet proto tcp from any to ($WAN_IF) port 80 -> 10.0.20.12 port 80 rdr on $WAN_IF inet proto tcp from any to ($WAN_IF) port 443 -> 10.0.20.12 port 443
 
You could add 'pass' to your rdr rules, then the redirected traffic will skip subsequent filtering rules
Yes, possible. But as you said, subsequent rules will be ignored. So a block rule, to thwart brute-force attacks for example, would never be applied.
 
Hi everyone

Thank you so much for all your replies, and especially to SirDice—I really appreciate it! I’m currently in the process of implementing your recommendations.

I’ve spent countless hours searching online for a good starting point. Many people go with a "block all" approach, but I’m not interested in setting up two pass rules for each direction.

Is my idea of using:
Code:
pass in log on $SERVER_IF inet proto { udp, tcp } from $SERVER_NET to ! <RFC1918> keep state
completely wrong? I’ve used this on pfSense for years, but maybe I’m overlooking a standard block rule somewhere.

So, what would the recommended configuration look like if I want to block first and then allow traffic without using two pass rules?

If I go with "block all," would these rules:
Code:
pass quick on $SERVER_IF proto tcp from $LAN_NET to 10.0.20.20 port { 22 }
still work? My idea of blocking based on source might not make much sense on my custom-built router, but router performance is not an issue. 😃

You’re more than welcome to share some of your own configurations as well!
 
Many people go with a "block all" approach,
A good alternative is to use block in on $WAN_IF, that would at least block all incoming connections from the internet.

completely wrong?
That rule is not wrong, but it would also block some other traffic you might want to allow. It's the nat rule that may be problematic, because it will also apply the nat to traffic that shouldn't exist on your network.
Code:
nat on $WAN_IF from <RFC1918> to any -> ($WAN_IF)
 
Back
Top