Abstract
The BSDs are known to have some wonderful built-in tools to the base operating system, and the tools I am going to cover today are no different. IPFW is one of the three firewalls built into the FreeBSD base system, and while it is a very solid firewall, it also has a network testing/shaping utility built in as well in the form of dummynet.
IPFW
Before enabling and trying to use IPFW, it is recommended to make sure we do not lock ourselves out of our systems. This is less important if you are locally working on a machine, but if you are accessing the machine via a network protocol (like SSH) firewall rules are a great way to lock yourself out. Generally, the recommended way to allow all traffic rather than blocking all traffic. The way I have had the most luck with is the running the following command before starting IPFW:
# Must be run as root
kenv net.inet.ip.fw.default_to_accept=1
Then enable IPFW by running either of the following commands as root:
# Must run as root
service ipfw onestart
# or running
kldload ipfw
Now that we have the basic firewall setup, let’s add some rules. I will now present one of the most secure, if not the most secure, firewall configurations possible. This is also the default configuration for IPFW:
ipfw -q add 65535 deny ip from any to any
In this configuration all network traffic on the machine is blocked, thus making it extremely secure. The biggest problem with this configuration is that it is also quite unhelpful. In the vast majority of cases in 2025 computer use will require a network at some point which the above rule will block. If this is the rule you are really wanting, just unplug the Ethernet cable and remove the wireless card from the machine. Assuming that some amount of networking is desired on the machine, let’s look at some rules that might be more useful. The following firewall rules will disable all traffic except for TCP traffic coming in on port 22:
ipfw -a add 00100 allow tcp from any to me 22 in via vtnet0 setup limit src-add 2
ipfw -a add 00200 deny ip from any to any
With this rule set, we are able to SSH into the machine, but are not able to do much of anything else. While that is not super useful, it does give remote access to the machine. More importantly, it shows an example of how firewall rules are processed in IPFW (and many other firewalls). The rules are processed by going down the rule numbers and when the packet comes across a rule that matches, the packet is then processed according to that rule. So, given the following rules:
00100 allow tcp from 192.168.122.1 to me 22 via vtnet0 setup limit src-addr 2 :default
00500 deny tcp from any to me via vtnet
65535 deny ip from any to any
We would see that rule ‘00100’ and rule ‘00500’ conflict (as well as the default deny all). However, I can still SSH into the machine because the packet is coming from 192.168.122.1 on port 22 to the correct interface. Once IPFW sees that the SSH packet matches that rule, it ceases to process the packet any further. It is very important to keep this in mind as rules may not be effective, or be too effective if not ordered appropriately.
Now that we have a very basic understanding of IPFW, let’s see about
making a basic firewall configuration for a public facing server. During
the process of developing your firewall rules, it is highly important to
not lock yourself out of the system. As stated above, the common method
of doing this is changing the default ‘block all’ to ‘allow all’,
however, it is possible that you will lock yourself out at some point
during the creation of these rules (especially once you turn the deny
all back on). So, as another way to prevent this, it might be worth
setting up an automated timed restart as a backup method of accessing
the system. The shutdown
command can do this, but after
running it no more user logins are allowed; an alternative that does not
run into this problem is using sleep
:
# Must run as root
sleep 15m && reboot &
This command will reboot the system after fifteen minutes, which will then revert IPFW to the previously known state (off if the service isn’t enabled). Adjust the time to whatever seems reasonable for you (or don’t use it at all at your own risk). To prevent the reboot, simply kill the sleep command:
# Must run as root
ps -aux | grep sleep
root 4139 0.0 0.2 14076 2116 0 SC 22:27 0:00.00 sleep 15m
root 4141 0.0 0.0 432 264 0 R+ 22:27 0:00.00 grep sleep
kill -9 4139
Example Firewall Script
I am not going to go too much into the development process for firewall rules as they will vary greatly depending on your needs. The ipfw man page or the FreeBSD docs page are both great places to find specific implementation details or how to go about something I may not cover in this blog post.
To setup IPFW rules, we need to set the firewall_script
variable in /etc/rc.conf
, this can be done by either
running:
sysrc firewall_script="/etc/ipfw.rules"
# or running
echo 'firewall_script="/etc/ipfw.rules"' >> /etc/rc.conf
This will make IPFW look to a script located at
/etc/ipfw.rules
for what it should do. The script location
can obviously change to another location if preferred. Let’s see what a
script for a basic webserver might look like:
#!/bin/sh
ipfw -q -f flush
cmd="ipfw -q add"
pif="vtnet0"
# Allow SSH traffic
$cmd 00100 allow tcp from any to me 22 in via $pif limit src-addr 2
# Allow bi-directional pings
$cmd 00200 allow icmp from any to any via $pif keep-state
# Allow DNS via UDP and TCP
$cmd 00300 allow udp from me to any 53 via $pif keep-state
$cmd 00350 allow tcp from me to any 53 via $pif keep-state
# Allow Receiving HTTP(S) 1, 2, and 3 (QUIC)
$cmd 00400 allow tcp from any to me 80 via $pif keep-state
$cmd 00400 allow tcp from any to me 443 via $pif keep-state
$cmd 00400 allow udp from any to me 80 via $pif keep-state
$cmd 00400 allow udp from any to me 443 via $pif keep-state
# Allow Serving HTTP(S) 1, 2, and 3 (QUIC)
$cmd 00400 allow tcp from me to any 80 via $pif keep-state
$cmd 00400 allow tcp from me to any 443 via $pif keep-state
$cmd 00400 allow udp from me to any 80 via $pif keep-state
$cmd 00400 allow udp from me to any 443 via $pif keep-state
$cmd 65535 deny all from any to any in via $pif
Admittedly, this is a very, very basic firewall script that will
probably not cover many people’s use-case. For one, SSH is allowed from
any address, which makes some people nervous. If this is the case, ‘any’
can be replaced with a specific IP or IP range. Additionally, there is
no logging enabled. That can be enabled by adding log
after
the allow or deny keyword. For example:
#!/bin/sh
ipfw -q -f flush
cmd="ipfw -q add"
pif="vtnet0"
# Allow SSH from one of Google's public IPs
$cmd 00100 allow tcp from 173.194.219.102 to me 22 in via $pif limit src-addr 2
# Allow SSH from any IP in 192.168.122.0/24
$cmd 00100 allow tcp from 192.168.122.0/24 to me 22 in via $pif limit src-addr 2
# Allow bi-directional pings
$cmd 00200 allow icmp from any to any via $pif keep-state
# Allow DNS via UDP and TCP
$cmd 00300 allow udp from me to any 53 via $pif keep-state
$cmd 00350 allow tcp from me to any 53 via $pif keep-state
# Allow Receiving HTTP(S) 1, 2, and 3 (QUIC)
$cmd 00400 allow tcp from any to me 80 via $pif keep-state
$cmd 00400 allow tcp from any to me 443 via $pif keep-state
$cmd 00400 allow udp from any to me 80 via $pif keep-state
$cmd 00400 allow udp from any to me 443 via $pif keep-state
# Allow Serving HTTP(S) 1, 2, and 3 (QUIC)
$cmd 00400 allow tcp from me to any 80 via $pif keep-state
$cmd 00400 allow tcp from me to any 443 via $pif keep-state
$cmd 00400 allow udp from me to any 80 via $pif keep-state
$cmd 00400 allow udp from me to any 443 via $pif keep-state
$cmd 65535 deny log all from any to any in via $pif
There are a few other notes about these firewall rules. I do
recommend accepting incoming web traffic, because blocking it will
prevent pkg
from operating properly. If you were
particularly paranoid you could remove the UDP rules for web traffic but
that would prevent QUIC from working as it is a UDP protocol.
Additionally, DNS is normally a UDP protocol, but will occasionally use
TCP. Blocking TCP traffic on port 53 would probably be fine,
but for most people I do not see a problem with allowing it through.
Additionally, you could block it for only certain IP addresses
(i.e. 9.9.9.9) and that would reduce the attack surface.
From here, we simply enable our firewall:
sysrc firewall_enable=1
# or
echo firewall_enable=1 >> /etc/rc.conf
and start the service (or you can reboot the machine if that’s somehow easier):
service ipfw start
You now have a working firewall, you can test that things are
actually being blocked by using tools such as telnet
or
nc
to try to connect to things using ports that should be
blocked. For example, you should not be able to connect to a mail server
with the above configuration.
Dummynet
Dummynet is a traffic shaping and network testing tool that is closely tied into IPFW. With Dummynet, it is possible to add latency, bandwidth limits, change scheduling algorithms, introduce packet loss, set queue size, and overall introduce limitations or other chaos into the network. Dummynet generally operates on either pipes or queues (or both). In general, pipes are for setting hard limitations, whereas queues can determine how flows will share available bandwidth.
Dummynet has a lot of stuff it can do, but in practice it is most often used for testing networks, so that is going to be what we will do in this post. For more information on the specifics or advanced usage of the tool, I might recommend the dnctl man page or the IPFW man page. For our example network, I wanted to emulate a connection to Mars, however, Dummynet will not allow for a delay over 10,000. So, instead I set the delay for 9,000 and a bandwidth of 2Mbp/s. We can create that with the following dummynet rules:
# Must run as root
# This command sets up the pipes for dummynet to set limits on
ipfw add 01000 pipe 1 ip from 192.168.122.65 to 192.168.122.1
ipfw add 01001 pipe 2 ip from 192.168.122.1 to 192.168.122.65
# Actually setting limitations
ipfw pipe 1 config delay 9000 bw 2Mbp/s
ipfw pipe 2 config delay 9000 bw 2Mbp/s
Now we can use tools like iperf3, ping, or sending files via scp to verify bandwidth and latency. Though, at those specs, most of these tools are fairly painful to use. Playing with the delays, bandwidth and queuing makes for an interesting experiment as well as testing things like websites in low resource environments. For example, seeing how your website loads on a satellite connection, or seeing how terrible SSH is with a 9,000ms delay.
More Resources
This is the part of the blog post where I admit that I am standing on the shoulders of giants for learning about these tools, and if this blog post does not cover a particular topic, or you are having trouble understanding it please look into the following sources (or check the man pages).