IPF configuration for IPv4 and IPv6

IPF - IPFILTER is a cross-platform open source firewall which is used in a couple of operating systems such as Solaris or FreeBSD. I had to discover this tool during a recent cyber exercise (Locked Shields) which uses common and less common technologies to simulate a realistic network under attack.

If you ever have to deal with FreeBSD, you will realize there are many options for host firewall and no drop-in replacement for Linux alternatives such as iptables. I decided to investigate further IPF because it just needs to be enabled at boot and the rules can easily be replaced during runtime, it also supports IPv6. This last point was critical for me as the host was running dual stack IPv4/6 on a single interface. However I found very little information in the official documentation about how to use IPv4 and IPv6 filtering together and how the rules interact with each other. Therefore in this article, I will explain how to deploy an allowed list host firewall on a FreeBSD 12 with dual stack.

Creating the rules

This is the main part you will need to customize according to your needs. In this article, we use an allowed list approach which means all connections will be blocked except the ones we specify explicitly.

Two files will need to be created:

  • /etc/ipf.rules
  • /etc/ipf6.rules

For IPv4 and IPv6 respectively. Here is what /etc/ipf.rules will look like in this example. Our main network interface is vmx0, you will need to replace it if you don't have the same interface name.

#################################################################
# No restrictions on Loopback Interface
#################################################################
pass in quick on lo0 all
pass out quick on lo0 all

# Replicate the above if you have internal interfaces (such as for Docker)

#################################################################
# Interface facing external (Outbound Section)
#################################################################

# DNS configuration
# Replace 1.1.1.1 with your DNS server
pass out quick on vmx0 proto tcp from any to 1.1.1.1 port = 53 flags S keep state
pass out quick on vmx0 proto udp from any to 1.1.1.1 port = 53 keep state


# HTTP OUTBOUND config
# Specify all servers that needs to be reached via HTTP (for instance update servers)
# You can also replace the IP by "any" to be less granular
# 46.4.72.43 is the IP of https://freebsd.pkgs.org/

pass out quick on vmx0 proto tcp from any to 46.4.72.43 port = 80 flags S keep state
pass out quick on vmx0 proto tcp from any to 46.4.72.43 port = 443 flags S keep state


# ALLOW oubtound NTP to NTP servers
# Modify it to your own NTP server IP
pass out quick on vmx0 proto udp from any to 45.87.77.15 port = 123 keep state


#################################################################
# Interface facing external (Inbound Section)
#################################################################

# Block all inbound traffic from non-routable or reserved address spaces

block in quick on vmx0 from 169.254.0.0/16 to any    #DHCP auto-config
block in quick on vmx0 from 192.0.2.0/24 to any      #reserved for docs
block in quick on vmx0 from 204.152.64.0/23 to any   #Sun cluster interconnect
block in quick on vmx0 from 224.0.0.0/3 to any       #Class D & E multicast

##### Block a bunch of different nasty things. ############
# That I do not want to see in the log

# Block frags
block in quick on vmx0 all with frags

# Block short tcp packets
block in quick on vmx0 proto tcp all with short

# block source routed packets
block in quick on vmx0 all with opt lsrr
block in quick on vmx0 all with opt ssrr

# Block nmap OS fingerprint attempts
# Log first occurrence of these so I can get their IP address
block in log first quick on vmx0 proto tcp from any to any flags FUP

# Block anything with special options
block in quick on vmx0 all with ipopts

# Block ident
block in quick on vmx0 proto tcp from any to any port = 113

# Block all Netbios service. 137=name, 138=datagram, 139=session
# Netbios is MS/Windows sharing services.
# Block MS/Windows hosts2 name server requests 81
block in log first quick on vmx0 proto tcp/udp from any to any port = 137
block in log first quick on vmx0 proto tcp/udp from any to any port = 138
block in log first quick on vmx0 proto tcp/udp from any to any port = 139
block in log first quick on vmx0 proto tcp/udp from any to any port = 81

# Allow incoming ICMP, feel free to disable it for extra security

pass in quick on vmx0 proto icmp from any to any keep state

# ALLOW input services
# Customize as you see fit for TCP and UDP services
# TCP example, we allow SSH from a private range
pass in quick on vmx0 proto tcp from 10.0.0.0/8 to any port = 22 flags S keep state

# UDP example
pass in quick on vmx0 proto udp from 10.0.0.0/8 to any port = 123 keep state


################### End of rules file #####################################

You will notice we don't express the default block rule in this file, the reason is because it will be done in the ipf6 rules which is evaluated second by the engine. If you put the default block rule in the ipv4 rules file, all ipv6 traffic will be blocked. This is something I had to figure out by myself as the documentation is not clear on this aspect.

Here is the example of what you can put in the /etc/ipf6.rulesfile.

#################################################################
# No restrictions on Loopback Interface
#################################################################
pass in quick on lo0 all
pass out quick on lo0 all

# You can add whitelisted interfaces (Docker)

#################################################################
# Interface facing external network (Outbound Section)
#################################################################

# DNS configuration
# Modify with your own DNS server(s)
pass out quick on vmx0 proto tcp from any to 2606:4700:4700::1111 port = 53 flags S keep state
pass out quick on vmx0 proto udp from any to 2606:4700:4700::1111 port = 53 keep state


# HTTP OUTBOUND config
# Add any IP that needs to be reachable or change to "any"
pass out quick on vmx0 proto tcp from any to 2a00:1450:400e:80a::200e port = 80 flags S keep state
pass out quick on vmx0 proto tcp from any to 2a00:1450:400e:80a::200e port = 443 flags S keep state

# This is required for IPv6 to work correctly
pass out quick on vmx0 proto ipv6-icmp from any to any keep state

# Block and log only the first occurrence of everything
# else that's trying to get out.
# This rule implements the default block
block out log quick on vmx0 all

#################################################################
# Interface facing external network (Inbound Section)
#################################################################

#Allow ICMP, otherwise IPv6 doesn't work
pass in quick on vmx0 proto ipv6-icmp from any to any keep state

# ALLOW input services
# TCP example, we allow SSH from anywhere on IPv6
pass in quick on vmx0 proto tcp from any to any port = 22 flags S keep state

# UDP example
pass in quick on vmx0 proto udp from any to any port = 123 keep state


# Block and log only first occurrence of all remaining traffic
# coming into the firewall. The logging of only the first
# occurrence avoids filling up disk with Denial of Service logs.
# This rule implements the default block.
block in log quick on vmx0 all
################### End of rules file #####################################

In this case, we clearly indicate that we block all remaining traffic, both on inbound and outbound ruleset. We also specify the log keyword so that these attempts can be reported. We first need to configure a few things as we will see in the next section.

Configure the logging

This is rather simple, we just need to indicate to syslog to record the rsyslog events coming out of local0, the default source for ipf. We first need to create the file with the correct permissions.

# touch /var/log/ipfilter.log
# chmod 600 /var/log/ipfilter.log

In /etc/syslog.conf, you need to add the following line:

local0.* /var/log/ipfilter.log

All the blocked attempts will appear in this file with some useful information such as source IP, source port, destination IP, destination port and protocol. This can be very helpful to fine tune your policy

Enabling the IPF firewall

The next step is to make sure the firewall is enabled at boot time. IPF can be loaded in the kernel during runtime but in this case I'm interested in a persistent configuration.

You will need to edit the /etc/rc.conf file and append the following lines:

ipfilter_enable="YES"             # Start ipf firewall
ipfilter_rules="/etc/ipf.rules"   # loads rules definition text file
ipv6_ipfilter_rules="/etc/ipf6.rules"
ipmon_enable="YES"                # Start IP monitor log
ipmon_flags="-Ds"

The ipmon part is not strictly required but it will allow to keep logs of traffic being accepted or blocked which can alert you on an ongoing attack. At this point if you reboot your BSD system, the firewall will be fully active with the rules defined and the logging.

Fine tuning the rules

Feel free to adjust the rules in /etc/ipf.rules and /etc/ipf6.rules as you discover you might be blocking legitimate traffic. Once you have made a modification, you do not need to reboot your system, you can simply run:

# service ipfilter reload

It will also warn you if syntax is incorrect. Be careful, using restart instead of reload will have a different effect (unlike on Linux distributions) and will close all connections which will kick you out of SSH!

Log rotations

If you want to avoid having an ever growing /var/log/ipfilter.log, you can put it on the watch list for the log rotation. You will need to modify /etc/newsyslog.conf and add the following line:

/var/log/ipfilter.log 600  10   1000000 *     JC

This will keep a file of maximum 1MB and keep 10 versions of history. Don't forget to restart the syslog daemon

# service restart syslogd