IPFW on FreeBSD: Firewall Review
IPFW (IP Firewall) is FreeBSD's native packet filtering firewall. It has been part of the FreeBSD kernel since the early 1990s, making it one of the oldest actively maintained firewalls in existence. Unlike PF, which was imported from OpenBSD, IPFW was developed specifically for FreeBSD and remains a FreeBSD-centric tool. It processes packets in-kernel with a numbered rule chain, supports stateful inspection, integrates with dummynet for traffic shaping and bandwidth management, and provides NAT through the in-kernel natd replacement (ipfw nat). On FreeBSD, IPFW is the firewall you use when you need fine-grained control over packet processing order, traffic shaping with dummynet pipes, or when you prefer the explicit numbered-rule model over PF's anchored approach. This review covers IPFW's architecture, rule syntax, stateful filtering, dummynet, NAT configuration, in-kernel processing, and how it compares with PF.
What IPFW Does
IPFW is a stateful packet filter that operates at the IP layer (Layer 3) and transport layer (Layer 4). It inspects every packet entering, leaving, or traversing the system and applies a sequential chain of rules to determine whether to allow, deny, or modify the packet.
Core capabilities:
- Stateful packet filtering -- track TCP connections and UDP pseudo-connections, allowing return traffic automatically without explicit rules for each direction.
- Numbered rule chain -- rules are numbered and processed in ascending order. The first matching rule determines the packet's fate (unless the rule is a
count,skipto, or other non-terminal action). - Dummynet traffic shaping -- create pipes and queues to limit bandwidth, add latency, simulate packet loss, and implement weighted fair queuing.
- In-kernel NAT -- translate source and destination addresses without a separate
natddaemon. - IP sets and tables -- efficient lookup tables for IP addresses, supporting thousands of entries without performance degradation.
- IPv6 support -- full IPv6 filtering with the same rule syntax.
- Logging -- log matched packets to syslog or to a dedicated
ipfw logfacility. - Dynamic rules -- created automatically by stateful rules (
keep-state,limit) and expire after a configurable timeout. - Forward -- redirect packets to a different destination IP or port, useful for transparent proxying.
IPFW does not perform deep packet inspection, application-layer filtering, or IDS/IPS functions. For those, pair it with Suricata or Snort.
Architecture
IPFW runs entirely in the FreeBSD kernel. When a packet enters the network stack, the kernel passes it through the IPFW rule chain before routing, local delivery, or forwarding. This in-kernel processing means:
- No context switches -- packet processing does not leave kernel space.
- Low latency -- rule evaluation adds microseconds, not milliseconds.
- High throughput -- IPFW can process millions of packets per second on modern hardware.
The rule chain is a flat, ordered list. Rules are numbered from 1 to 65535. Rule 65535 is always the last rule and is configured system-wide to either allow or deny by default. The ipfw userland tool manipulates the in-kernel rule set.
Enabling IPFW on FreeBSD
Load the Kernel Module
shkldload ipfw
To load IPFW at boot with a default-allow policy (safe for remote configuration):
shsysrc firewall_enable="YES" sysrc firewall_type="open"
The firewall_type="open" setting loads a permissive ruleset first, preventing lockout. Change to a custom script once your rules are tested.
Custom Firewall Script
shsysrc firewall_type="/etc/ipfw.rules"
Create your rule script:
shcat > /etc/ipfw.rules << 'EOF' #!/bin/sh # Flush existing rules ipfw -q flush # Loopback ipfw -q add 100 allow all from any to any via lo0 ipfw -q add 200 deny all from any to 127.0.0.0/8 ipfw -q add 300 deny all from 127.0.0.0/8 to any # Stateful rules - allow established connections ipfw -q add 500 check-state :default # Allow outgoing traffic and create state ipfw -q add 1000 allow tcp from me to any setup keep-state :default ipfw -q add 1100 allow udp from me to any keep-state :default ipfw -q add 1200 allow icmp from me to any keep-state :default # Allow incoming SSH ipfw -q add 2000 allow tcp from any to me 22 setup keep-state :default # Allow incoming HTTP/HTTPS ipfw -q add 2100 allow tcp from any to me 80,443 setup keep-state :default # Allow incoming ICMP ping ipfw -q add 3000 allow icmp from any to me icmptypes 8 # Deny everything else ipfw -q add 65000 deny log all from any to any EOF chmod 755 /etc/ipfw.rules
Kernel Configuration
For compiled-in IPFW (instead of the kernel module), add to your kernel configuration:
shoptions IPFIREWALL options IPFIREWALL_VERBOSE options IPFIREWALL_VERBOSE_LIMIT=100 options IPFIREWALL_DEFAULT_TO_ACCEPT options IPDIVERT options DUMMYNET
The IPFIREWALL_DEFAULT_TO_ACCEPT option makes rule 65535 an allow rule, which is critical during initial configuration to prevent lockout.
Rule Syntax
IPFW rules follow a consistent format:
shellipfw add [number] [action] [log] [proto] from [src] to [dst] [options]
Actions
allow/permit/accept-- pass the packet.deny/drop-- silently discard the packet.reject-- discard and send an ICMP unreachable (TCP RST for TCP).count-- increment counters but continue processing (non-terminal).skipto N-- jump to rule number N.divert PORT-- send to a divert socket (used bynatd).pipe N/queue N-- send to dummynet pipe or queue for traffic shaping.fwd IP:PORT-- forward to a different destination.nat N-- apply NAT instance N.
Protocol Specification
sh# TCP traffic to port 80 ipfw add 1000 allow tcp from any to me 80 # UDP traffic to DNS ipfw add 1100 allow udp from any to me 53 # ICMP echo request ipfw add 1200 allow icmp from any to me icmptypes 8 # All IP traffic ipfw add 1300 allow ip from 10.0.0.0/24 to any
Source and Destination
sh# Specific IP ipfw add 1000 allow tcp from 192.168.1.100 to me 22 # Subnet ipfw add 1100 allow tcp from 10.0.0.0/8 to me 22 # Multiple ports ipfw add 1200 allow tcp from any to me 80,443,8080 # Port range ipfw add 1300 allow tcp from any to me 1024-65535 # Any address on this machine ipfw add 1400 allow tcp from any to me 22
TCP Flags
sh# Only SYN packets (new connections) ipfw add 1000 allow tcp from any to me 80 setup # Established connections (ACK set) ipfw add 1100 allow tcp from any to me established
Stateful Filtering
Stateful filtering tracks connections so you only need rules for the initial packet. Return traffic is matched by dynamic rules created by keep-state.
Basic Stateful Configuration
sh# Check dynamic rules first ipfw add 100 check-state :default # Allow outgoing connections and create state ipfw add 1000 allow tcp from me to any setup keep-state :default ipfw add 1100 allow udp from me to any keep-state :default # Allow incoming SSH and create state ipfw add 2000 allow tcp from any to me 22 setup keep-state :default # Deny everything else ipfw add 65000 deny all from any to any
When a packet matches rule 1000 (outgoing TCP), IPFW creates a dynamic rule that matches return traffic. The check-state at rule 100 catches these dynamic rules early, avoiding unnecessary processing of the full rule chain.
Connection Limits
Limit concurrent connections from a single source:
sh# Limit SSH to 3 connections per source IP ipfw add 2000 allow tcp from any to me 22 setup limit src-addr 3
This prevents brute-force attacks by limiting how many simultaneous connections a single IP can maintain.
Dynamic Rule Timeouts
Configure timeouts for stateful rules:
shsysctl net.inet.ip.fw.dyn_syn_lifetime=15 sysctl net.inet.ip.fw.dyn_ack_lifetime=300 sysctl net.inet.ip.fw.dyn_udp_lifetime=30 sysctl net.inet.ip.fw.dyn_short_lifetime=10
Dummynet Traffic Shaping
Dummynet is IPFW's integrated traffic shaping system. It creates pipes (fixed bandwidth) and queues (weighted fair queuing within a pipe) to control traffic flow.
Load Dummynet
shkldload dummynet
Or add to /boot/loader.conf:
shdummynet_load="YES"
Bandwidth Limiting
Limit a server's outgoing bandwidth to 10 Mbps:
shipfw pipe 1 config bw 10Mbit/s ipfw add 1000 pipe 1 ip from me to any out
Limit incoming bandwidth to 50 Mbps:
shipfw pipe 2 config bw 50Mbit/s ipfw add 1100 pipe 2 ip from any to me in
Per-Client Bandwidth Limits
Give each client IP a maximum of 1 Mbps:
shipfw pipe 1 config bw 1Mbit/s mask dst-ip 0xffffffff ipfw add 1000 pipe 1 ip from me to any out
The mask dst-ip 0xffffffff creates a separate pipe instance for each unique destination IP.
Simulating Network Conditions
Dummynet can simulate latency, jitter, and packet loss -- invaluable for testing:
sh# Add 50ms latency and 1% packet loss ipfw pipe 1 config delay 50ms plr 0.01 ipfw add 1000 pipe 1 ip from any to any
Weighted Fair Queuing
Create a pipe and distribute bandwidth among queues with different weights:
shipfw pipe 1 config bw 100Mbit/s ipfw queue 1 config pipe 1 weight 50 ipfw queue 2 config pipe 1 weight 30 ipfw queue 3 config pipe 1 weight 20 # High priority traffic (SSH) ipfw add 1000 queue 1 tcp from any to me 22 # Normal traffic (HTTP) ipfw add 1100 queue 2 tcp from any to me 80,443 # Low priority (everything else) ipfw add 1200 queue 3 ip from any to me
SSH gets 50% of bandwidth, HTTP gets 30%, and everything else gets 20% under contention. When a queue is idle, others can use its share.
In-Kernel NAT
IPFW provides in-kernel NAT since FreeBSD 7.0, replacing the older natd userspace daemon.
Basic NAT Configuration
shsysctl net.inet.ip.forwarding=1 sysrc gateway_enable="YES" # Configure NAT instance ipfw nat 1 config if em0 reset same_ports deny_in # Apply NAT to outgoing traffic ipfw add 100 nat 1 ip from 192.168.1.0/24 to any out via em0 ipfw add 200 nat 1 ip from any to any in via em0
The if em0 option uses em0's IP address as the NAT address. same_ports tries to preserve source port numbers. deny_in drops incoming connections that do not match an existing NAT state.
Port Forwarding
Forward incoming port 80 to an internal server:
shipfw nat 1 config if em0 redirect_port tcp 192.168.1.50:80 80
Forward a port range:
shipfw nat 1 config if em0 redirect_port tcp 192.168.1.50:3000-3010 3000-3010
Tables
Tables provide efficient IP lookups for large rule sets:
sh# Create and populate a table ipfw table 1 create type addr ipfw table 1 add 192.168.1.0/24 ipfw table 1 add 10.0.0.0/8 ipfw table 1 add 172.16.0.0/12 # Use in rules ipfw add 1000 allow tcp from 'table(1)' to me 22
Tables support thousands of entries with O(log n) lookup time. They are ideal for maintaining large allow/deny lists without creating thousands of individual rules.
Dynamic Tables for Blocklists
Load a blocklist from a file:
shipfw table 2 create type addr while read ip; do ipfw table 2 add "$ip" done < /usr/local/etc/blocklist.txt ipfw add 500 deny all from 'table(2)' to any
IPFW vs PF
FreeBSD supports both IPFW and PF. The choice depends on your requirements and background:
Choose IPFW when:
- You need dummynet traffic shaping. PF has ALTQ, but dummynet is more flexible and better maintained on FreeBSD.
- You prefer explicit numbered rules with predictable processing order.
- You need in-kernel NAT with complex port forwarding.
- You are managing a FreeBSD router or gateway with bandwidth management requirements.
- You want to use tables with dynamic entries for large blocklists.
Choose PF when:
- You want a cleaner, more readable configuration syntax.
- You are coming from OpenBSD and already know PF.
- You need anchors for modular rule management (loading/unloading rule subsets).
- You want
pf.conf-style macro and table definitions in a single file. - You are building a firewall appliance (OPNsense and pfSense use PF).
Technical differences:
- IPFW processes rules sequentially with first-match semantics (unless
skiptois used). PF evaluates all rules and applies the last match (unlessquickis specified). - IPFW's rule numbering makes insertion and reordering explicit. PF rules are ordered by position in the config file.
- IPFW's dummynet is more capable than PF's ALTQ for traffic shaping.
- PF's syntax is more concise for complex rulesets. IPFW requires more verbose rules for equivalent functionality.
- Both support IPv4 and IPv6. Both support stateful filtering.
Monitoring and Debugging
View Rules with Counters
shipfw -a list
This shows each rule with packet and byte counters. Rules with zero counters may be unnecessary.
View Dynamic Rules
shipfw -d list
Reset Counters
shipfw zero
Logging
Rules with the log keyword send matching packets to syslog:
shipfw add 65000 deny log all from any to any
View logged packets:
shgrep ipfw /var/log/security
For verbose logging with packet contents:
shsysctl net.inet.ip.fw.verbose=1 sysctl net.inet.ip.fw.verbose_limit=100
Verdict
IPFW is FreeBSD's native firewall, deeply integrated with the kernel and unmatched in traffic shaping capabilities through dummynet. Its numbered-rule model provides explicit, predictable packet processing, and its in-kernel NAT handles complex gateway configurations. For FreeBSD systems that need bandwidth management, QoS, or network simulation alongside firewalling, IPFW is the right tool.
PF offers a more elegant configuration syntax and is the better choice for pure firewalling on servers that do not need traffic shaping. For gateway and router roles, IPFW with dummynet is the stronger option. Both are production-ready, well-maintained, and fully supported on FreeBSD.
Frequently Asked Questions
Can I run IPFW and PF simultaneously on FreeBSD?
Technically possible, but strongly discouraged. Both firewalls process packets independently, making rule interaction unpredictable. Choose one and disable the other.
How do I avoid locking myself out when configuring IPFW remotely?
Start with firewall_type="open" to load a permissive ruleset. Test new rules interactively with ipfw add. Use a cron job that resets the firewall after 5 minutes: echo "ipfw -q flush && ipfw -q add allow all from any to any" | at now + 5 minutes. Once rules are verified, save them to your script and remove the safety net.
Does IPFW work in FreeBSD jails?
IPFW is a kernel-level firewall. It runs on the host and filters traffic for all jails. Individual jails cannot load or manage IPFW rules unless granted specific privileges with allow.raw_sockets. PF has similar limitations.
How many rules can IPFW handle?
IPFW supports up to 65535 rule numbers. Performance remains excellent with thousands of rules, especially when using tables for large IP sets and skipto for rule chain optimization.
Can IPFW do application-layer filtering?
No. IPFW operates at layers 3 and 4 (IP and TCP/UDP). For application-layer filtering (HTTP content inspection, DNS query filtering), use a proxy (Squid, Unbound) in conjunction with IPFW.
How do I persist IPFW rules across reboots?
Set sysrc firewall_enable="YES" and sysrc firewall_type="/etc/ipfw.rules". The script at /etc/ipfw.rules is executed at boot. Alternatively, use sysrc firewall_script="/etc/ipfw.rules" for the same effect.