SoCruel.NU

The domain that loves BSD

Home About Me Archive Contact

How to implement an internet facing FreeBSD IPFW firewall

Introduction

I am a FreeBSD user since version 2.2. I used IPFilter as my first packet filtering firewall on FreeBSD. It is still in the FreeBSD base system, but I guess not widely used anymore. When PF was ported to FreeBSD, I started using that and have used it ever since.

So I’ve never used the FreeBSD native packet filtering firewall IPFW before.

But that changed a little while ago, so I’ve decided to write about it and share my insights and gained knowledge about this subject. So this blog post is about implementing an internet facing firewall using FreeBSD IPFW! And we do this by using an example design which is described in the first 3 sections below (Diagram, The requirements for this setup and, Some starting points).

Diagram

The (imaginary) setup used for this blog post is shown in the diagram below.

Setup IPFW firewall

The requirements for this setup

The high level requirements for this setup are:

  • Use an up to date FreeBSD version 12.1
  • Use IPFW as packet filtering firewall
  • Use IPFW In-kernel NAT
  • Only IP version 4 is used
  • The firewall is setup using the default deny stance, so (only) traffic which is explicitly allowed is defined and everything else is blocked
  • The firewall has 2 network interfaces
    • an external public internet facing network interface
    • an internal private network interface
  • The firewall protects 3 internal networks from the internet:
    • a network with (mobile) clients
    • a network with FreeBSD based servers and
    • a network with different IoT devices
  • It is asummed that these 3 networks are connected to an internal router which is also connected to the network of the firewall internal interface em1
  • The firewall ruleset must not only protect the 3 networks but also the firewall itself
  • The firewall itself can only be accessed by the defined management hosts using SSH
  • Outgoing NTP traffic is allowed to the whole internet
  • Outgoing DNS traffic is only allowed from
  • All denied (or blocked) traffic is logged on the firewall through syslog and send to a central log host
  • It must be possible to reload the ruleset with an active SSH session to the firewall

Some starting points

To be able to fill in the details for the IPFW firewall ruleset we define some starting points. These starting points are:

  • The internet facing public network interface of the firewall is: em0
  • The internal facing private network interface of the firewall is: em1
  • The internet public IP address of the firewall is: x.y.z.254/29
  • The internal private IP address of the firewall is: a.b.c.254/24
  • The private IP address of the internal router in the a.b.c.0/24 network is: a.b.c.1/24
  • The default gateway of the firewall is set to the IP address of the internet router: x.y.z.249/29 (the public subnet is x.y.z.248/29)
  • The private IP address range of the (mobile) client network is: f.g.h.0/24
  • The private IP address range of the server network is: p.q.r.0/24
  • The private IP address range of the IoT devices network is: f.g.i.0/24
  • The private IP addresses of the internal name servers are: p.q.r.51 and p.q.r.52
  • The private IP addresses of the management hosts are: f.g.h.201 and f.g.h.202
  • The private IP address of the internal log host is: p.q.r.41
  • The public ISP caching name servers are: M.N.O.6 and M.N.Q.9
  • The public ISP NTP servers are: M.N.O.22 and M.N.Q.22

And last but not least: it is assumed that all commands are done using the root user of the firewall.

Base setup of the firewall

Some other important aspects of the firewall host configuration are covered first, before we dive into the implementation of IPFW:

Setup of the interfaces

The interfaces of the firewall are setup like:

# sysrc ifconfig_em0="inet x.y.z.254 netmask 255.255.255.248"
# sudo sysrc ifconfig_em1="inet a.b.c.254 netmask 255.255.255.0"

Default gateway

We set the default gateway to the IP address of the internet router:

# sysrc defaultrouter="x.y.z.249"

Static routes

And we also have to define some static routes on the firewall to be able to reach the 3 internal networks:

# sysrc static_routes="clients servers iot"
# sysrc route_clients="f.g.h.0/24 a.b.c.1"
# sysrc route_servers="p.q.r.0/24 a.b.c.1"
# sysrc route_iot="f.g.i.0/24/24 a.b.c.1"

Setup of syslog

We have a requirement to log all IPFW blocked packets so we enable system logging:

# sysrc syslogd_enable="YES"
# sysrc syslogd_flags="-s"

And our base /etc/syslog.conf file looks like:

\*.err;kern.warning;auth.notice  /dev/console
\*.info;authpriv.none;kern.debug;security.none   /var/log/messages
cron.*                  /var/log/cron.log
authpriv.*              /var/log/auth.log
daemon.*                /var/log/daemon.log
include                 /etc/syslog.d
include                 /usr/local/etc/syslog.d

Setup of sshd

We want to access the firewall through SSH on its internal interface only, so we harden the default SSH configuration a bit:

# cat >> /etc/sshd_config << EOF
ListenAddress a.b.c.254
AllowGroups wheel
Protocol 2
X11Forwarding no
IgnoreRhosts yes
PermitEmptyPasswords no
PermitRootLogin no
ChallengeResponseAuthentication no
AllowAgentForwarding no
UseDNS no
MaxSessions 2
MaxAuthTries 2
LogLevel VERBOSE
Compression no
ClientAliveCountMax 2
AllowTcpForwarding no
EOF

The setup of IPFW

The first item to configure for an IPFW based firewall is to make sure that the FreeBSD operating system can route packets between the 2 interfaces:

# sysrc gateway_enable="YES"

Next is to enable IPFW and set the name of the script for the IPFW firewall rules:

# sysrc firewall_enable="YES"
# sysrc firewall_script="/etc/ipfw.rules"

And we also want to do Network Address Translation. This is enabled through the command:

# sysrc firewall_nat_enable="YES"

As we have enabled Network Address Translation, we also have to set the following kernal state variables:

# echo 'net.inet.tcp.tso=0' >> /etc/sysctl.conf
# echo 'net.inet.ip.fw.one_pass=0' >> /etc/sysctl.conf

We also want to enable and configure logging of blocked packets. The first step is to enable the firewall logging:

# sysrc firewall_logging="YES"

To enable logging to system logging we have to set the following kernel state variable:

# echo 'net.inet.ip.fw.verbose=1' >> /etc/sysctl.conf

With this IPFW logs to the security facility. And we do not want any limits for our logging, so we also set:

# echo 'net.inet.ip.fw.verbose_limit=0' >> /etc/sysctl.conf

And we also have to configure system logging:

# cat << EOF > /etc/syslog.d/ipfw.conf
security.*   /var/log/ipfw.log
security.*   @p.q.r.41:514
EOF

The first line defines that IPFW logs to the file /var/log/ipfw.log and the second line defines that we also send these logs to our loghost (with IP address p.q.r.41).

We are almost there, only 1 item to configure before we can go and define our firewall ruleset. We want to be able to reload (or restart) our IPFW rules while we have a SSH session with the firewall. To be able to do this we need to set a kernel state variable:

# echo 'net.inet.ip.fw.dyn_keep_states=1' >> /etc/sysctl.conf

The IPFW rules

The IPFW ruleset is a shell script and we have configured that the file of this script is /etc/ipfw.rules. The rights on this file are 0644 and the file is owned by the root user and wheel group.

The IPFW ruleset we build consists of the following parts and each part is discussed in more detail below:

  • start of the rules script
  • variables
  • start of the IPFW rules
  • the inbound rules
  • the outbound rules
  • the final part of the IPFW rules

We define the inbound rules before the outbound rules as packets always arrive inbound first on an interface.

The start of the rules script

The start of the IPFW rules script consists of the following lines:

#!/bin/sh

# Flush out the list before we begin.
/sbin/ipfw -q -f flush

The first line states that this is a /bin/sh shell script. And the real first command flushes the rules, such that we always start with a clean ruleset.

Variables

Before we start with the actual firewall rules we first set some (example) variables (see below). Most of these variables are self explanatory and come from the The starting points chapter above.

# Set variables
varIPFW="/sbin/ipfw"
varCMD="/sbin/ipfw -q add"
varSkip="skipto 09999"
varNICPub="em0"
varNICPri="em1"
varIPPub="x.y.z.254"
varIPPri="a.b.c.254"
varISPNameServers="M.N.O.6, M.N.Q.9"
varISPTimeServers="M.N.O.22, M.N.Q.22"
varInternalNameServers="p.q.r.51, p.q.r.52"
varMgmtHosts="f.g.h.201, f.g.h.202"
varPublicNet="x.y.z.248/29"
varClientsNet="f.g.h.0/24"
varServerNet="p.q.r.0/24"
varIoTNet="f.g.i.0/24"
varTrustedNets="f.g.h.0/24, p.q.r.0/24, f.g.i.0/24"
varSyslogHost="p.q.r.41"
varInternalRouter="a.b.c.1"
varRFC1918="10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16"
varServerNetTCPPortsAllowed="80, 443"
varClientsNetTCPPortsAllowed="22, 25, 80, 110, 143, 443, 465, 554, 587, 989, 990, 993, 995"
varClientsNetUDPPortsAllowed="80, 443"
varIoTNetTCPPortsAllowed="80, 443"
varIoTNetUDPPortsAllowed="53, 123"

The start of the IPFW configuration

So with all the variables known and filled in we can start with the actual ruleset for our internet facing firewall. In the first line we disable one_pass, which makes sure that the packet is reinjected into the firewalll at the next rules, which we want.

${varIPFW} disable one_pass

In the second line we define a NAT instance (with number 1) for incoming traffic on the external public facing interface (in this case em0 or ${varNICPub}):

${varIPFW} -q nat 1 config if ${varNICPub} same_ports unreg_only reset

Next we define our first numbered rules in which we set no restrictions on the loopback interface of the firewall:

# No restrictions on Loopback Interface
${varCMD} 00010 allow all from any to any via lo0
${varCMD} 00011 deny all from any to 127.0.0.0/8
${varCMD} 00012 deny ip from 127.0.0.0/8 to any

Then we queue and reassable possible IPv4 fragments (see more detail in the IPFW manual page):

# Reassemble inbound packets
${varCMD} 00099 reass all from any to any in

And define the NAT for any IPv4 inbound packets (the number must be the same as above!):

# NAT any IPv4 inbound packets
${varCMD} 00100 nat 1 ip4 from any to any in recv ${varNICPub}

Now it is time to check the packets against the dynamic ruleset (aka check-state):

# Check the state
${varCMD} 00101 check-state

The last part of the start of our IPFW configuration is to deny all partial packets:

# Deny partial packets
${varCMD} 00102 deny ip from any to any frag
${varCMD} 00103 deny ip from any to any established

The inbound rules

After we have setup all the basics it is now time to define the inbound rules. In this example we use the following rule numbers for our inbound rules:

  • 01000 - 01499: for inbound traffic on the public network interface (em0 or ${varNICPub})
  • 01500 - 01999: for inbound traffic on the private network interface (em1 or ${varNICPri})

And both rule numbers 1499 and 1999 are used for the deny all rule on the interface.

Public (external) interface

We start with defining the rules for (known) traffic which we do not want to log for the public network interface, like i.e. Multicast addresses:

# Deny and do NOT log known traffic
${varCMD} 01001 deny ip from ${varPublicNet} to 224.0.0.1 in recv ${varNICPub}

If needed or required you can add any additional rules here.

For this example we only allow incoming ICMP traffic on the public interface (from the whole internet):

# Allow inbound ICMP
${varCMD} 01301 allow icmp from any to me in recv ${varNICPub} keep-state

And we close of with the deny rule for incoming traffic on the public facing interface (em0):

# Deny all other in
${varCMD} 01499 deny log all from any to any in via ${varNICPub}

Private (internal) interface

So with the external interface all done, we can define the inbound rules on the internal interface. In this example we do not define traffic which we do not want to log on the internal interface (if you hae a requirement yourslef you can add them ofcourse). We first allow SSH traffic to the firewall coming from the management hosts (and limit the number of source addresses to 2!):

# Allow inbound SSH connections
${varCMD} 01501 allow tcp from ${varMgmtHosts} to me dst-port ssh in recv ${varNICPri} setup limit src-addr 2

Next we allow ICMP traffic from all the internal networks and the internal router to the internal interface of the firewall (em1):

# Allow inbound ICMP
${varCMD} 01701 allow icmp from ${varTrustedNets}, ${varInternalRouter} to me in recv ${varNICPri} keep-state

We use the next rule to allow traffic to the internet from our internal networks and the internal router. At this stage in the ruleset the outgoing Network Address Translation still needs to happen! We allow all IP based traffic here and the allowed traffic will be narrowed down at a later stage in the ruleset when we define the outgoing traffic on the external interface.

# Allow IP traffic on internal interface
${varCMD} 01899 allow ip from ${varTrustedNets}, ${varInternalRouter} to \( not ${varRFC1918} or not me \) in recv ${varNICPri}

And as the last rule for the incoming traffic on the internal interface we block and log all traffic:

# Deny all other in
${varCMD} 01999 deny log all from any to any in via ${varNICPri}

The outbound rules

The inbound rules are all defined, so we now continue with the outbound rules. In this example we use the following rule numbers for our outbound rules:

  • 02000 - 02499: for outbound traffic on the public network interface (em0 or ${varNICPub})
  • 02500 - 02999: for outbound traffic on the private network interface (em1 or ${varNICPri})

And both rule numbers 2499 and 2999 are used for the deny all rule on the interface.

Public (external) interface

We define the outbound rules on the public external interface for traffic coming from the firewall itself first:

# From me (DNS, NTP, HTTP(S), ICMP)
${varCMD} 02001 allow tcp from me to ${varISPNameServers} dst-port domain out xmit ${varNICPub} setup keep-state
${varCMD} 02002 allow udp from me to ${varISPNameServers} dst-port domain out xmit ${varNICPub} keep-state
${varCMD} 02011 allow udp from me to ${varISPTimeServers} dst-port ntp out xmit ${varNICPub} keep-state
${varCMD} 02021 allow tcp from me to any dst-port http, https out xmit ${varNICPub} setup keep-state
${varCMD} 02031 allow icmp from me to any out xmit ${varNICPub} keep-state

These rules allow the firewall itself to:

  • perform domain name resolution using the appointed ISP caching name servers
  • keep time using the appointed NTP servers
  • ‘browse’ the internet (using HTTP and HTTPS), e.g. used for FreeBSD updates and package updates
  • use ICMP or ping to the internet

With the outgoing traffic flows for the firewall itself all done, it is now time to define the allowed traffic from all the internal networks ‘behind’ the firewall. And we begin with the DNS traffic:

# Allow DNS out
${varCMD} 02101 ${varSkip} tcp from ${varInternalNameServers} to any dst-port domain out xmit ${varNICPub} setup keep-state
${varCMD} 02102 ${varSkip} udp from ${varInternalNameServers} to any dst-port domain out xmit ${varNICPub} keep-state
${varCMD} 02103 ${varSkip} tcp from ${varIoTNet} to any dst-port domain out xmit ${varNICPub} setup keep-state
${varCMD} 02104 ${varSkip} udp from ${varIoTNet} to any dst-port domain out xmit ${varNICPub} keep-state

The first 2 rules allow the internal name servers to perform name resolution to any name server on the internet. And this is allowed as well from the whole IoT network (see rule numbers 02103 and 02104, we assume that we can not configure the DNS settings of the IoT devices we have!).

We also allow NTP traffic out from all our internal systems:

# Allow NTP out
${varCMD} 02111 ${varSkip} udp from ${varTrustedNets}, ${varInternalRouter} to any dst-port ntp out xmit ${varNICPub} keep-state

With the DNS and NTP traffic defined we now define the allowed traffic from each of the internal networks:

The allowed traffic from our server network:

# Allowed traffic from server network
${varCMD} 02201 ${varSkip} tcp from ${varServerNet} to any dst-port ${varServerNetTCPPortsAllowed} out xmit ${varNICPub} setup keep-state

The allowed traffic from our clients network:

# Allowed traffic from clients network
${varCMD} 02211 ${varSkip} tcp from ${varClientsNet} to any dst-port ${varClientsNetTCPPortsAllowed} out xmit ${varNICPub} setup keep-state
${varCMD} 02212 ${varSkip} udp from ${varClientsNet} to any dst-port ${varClientsNetUDPPortsAllowed} out xmit ${varNICPub} keep-state

The allowed traffic from our IoT network:

# Allowed traffic from IoT network
${varCMD} 02221 ${varSkip} tcp from ${varIoTNet} to any dst-port ${varIoTNetTCPPortsAllowed} out xmit ${varNICPub} setup keep-state

And allow TCP traffic from the internal router:

# Allowed traffic from internal router
${varCMD} 02231 ${varSkip} tcp from ${varInternalRouter} to any dst-port http, https out xmit ${varNICPub} setup keep-state

And as the last allowed rule we allow ICMP traffic out from all internal networks:

# Allow ICMP out
${varCMD} 02301 ${varSkip} icmp from ${varTrustedNets}, ${varInternalRouter} to any out xmit ${varNICPub} keep-state

And as the last rule for the outgoing traffic on the external interface we block and log all traffic:

# Deny all other out
${varCMD} 02499 deny log all from any to any out via ${varNICPub}

Private (internal) interface

We define the outbound rules on the private internal interface for traffic coming from the firewall itself first:

# Allow syslog out
${varCMD} 02501 allow udp from me to ${varSyslogHost} dst-port syslog out xmit ${varNICPri} keep-state

This rule allows the firewall itself to log to the defined syslog host. We have no other requirements for outbound traffic on the private internal interface so the next rule is to deny all outgoing traffic from this interface:

# Deny all other out
${varCMD} 02999 deny log all from any to any out via ${varNICPri}

The last part of the IPFW configuration

We have to add 3 more rules to finish our IPFW firewall ruleset. And the first is to have a deny everything rule:

# Default deny all
${varCMD} 04999 deny log all from any to any

The second, and very important, rule is the rule for the outbound Network Address Translation. This rule is used by the outbound rules on the public external interface (se rule numbers 02101 - 02301) which have the ${varSkip} variable in the rule!

# Outbound IPv4 NAT
${varCMD} 09999 nat 1 ip4 from any to any out xmit ${varNICPub}

And to allow all the return traffic we have to specify the last rule below:

# Allow all
${varCMD} 10001 allow ip4 from any to any

Note: the above rule is mentioned in a lot of IPFW documentation, but I have not found a proper explanation why this rule is needed. So I’ve experimented with it not using it. But I could not get my rule set working without this rule. I saw that no return traffic was happening without this rule, so that must be a valid explanation.

The complete ruleset

The complete ruleset as described above is also available in a seperate file which you can find here.

Some handy IPFW commands

When you have done the IPFW implementation, you also want to see what ruleset is loaded and which rules are used (or not). Luckily provides IPFW some commands for this.

To see our loaded ruleset we can issue the command:

# ipfw list

And to include accounting records use the command:

# ipfw -a list

This same is accomplished with the command: # ipfw show.

And to also include the last time a chain entry was matched us the command:

# ipfw -at list

To clear the accounting packet counters use the command:

# ipfw zero

To reload the rules in the /etc/ipfw.rules file you can use:

# service ipfw restart

Please be aware that this last command can only be used if you have set the net.inet.ip.fw.dyn_keep_states kernel state variable to 1!

Wrap up

An example configuration of an internet facing FreeBSD IPFW firewall is described above. A whole lot more is possible with IPFW, like e.g.:

  • add the usage of tables to the ruleset
  • add rules for incoming traffic to e.g. a web server which is behind the firewall
  • add traffic shaping capabilities to the firewall

Resources

Some (other) resources about this subject:

Updated: November 12, 2020