FreeBSD.software
Home/Guides/IPFW on FreeBSD: Firewall Review
review·2026-04-09·13 min read

IPFW on FreeBSD: Firewall Review

In-depth review of IPFW on FreeBSD: rule syntax, stateful filtering, dummynet traffic shaping, NAT, in-kernel processing, and comparison with PF and IPFilter.

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 natd daemon.
  • 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 log facility.
  • 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

sh
kldload ipfw

To load IPFW at boot with a default-allow policy (safe for remote configuration):

sh
sysrc 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

sh
sysrc firewall_type="/etc/ipfw.rules"

Create your rule script:

sh
cat > /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:

sh
options 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:

shell
ipfw 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 by natd).
  • 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:

sh
sysctl 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

sh
kldload dummynet

Or add to /boot/loader.conf:

sh
dummynet_load="YES"

Bandwidth Limiting

Limit a server's outgoing bandwidth to 10 Mbps:

sh
ipfw pipe 1 config bw 10Mbit/s ipfw add 1000 pipe 1 ip from me to any out

Limit incoming bandwidth to 50 Mbps:

sh
ipfw 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:

sh
ipfw 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:

sh
ipfw 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

sh
sysctl 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:

sh
ipfw nat 1 config if em0 redirect_port tcp 192.168.1.50:80 80

Forward a port range:

sh
ipfw 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:

sh
ipfw 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 skipto is used). PF evaluates all rules and applies the last match (unless quick is 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

sh
ipfw -a list

This shows each rule with packet and byte counters. Rules with zero counters may be unnecessary.

View Dynamic Rules

sh
ipfw -d list

Reset Counters

sh
ipfw zero

Logging

Rules with the log keyword send matching packets to syslog:

sh
ipfw add 65000 deny log all from any to any

View logged packets:

sh
grep ipfw /var/log/security

For verbose logging with packet contents:

sh
sysctl 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.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.