🔥 ipfw: The Other FreeBSD Firewall
Numbered rules where the first match wins, tables, logging and built-in traffic shaping, tested with real traffic, and the way not to lock yourself out
Two firewalls in one system
FreeBSD ships three firewalls (pf, ipfw and ipfilter, from the documentation). You met pf on the pf page. This page is about ipfw, the one that is FreeBSD's own: older, simpler in some ways and with one feature that the others lack, traffic shaping (dummynet) built in. Run one firewall per machine, not both: two sets of rules at once are a headache to reason about. So the first thing we do is turn pf off.
The big difference you must learn is this:
- pf: rules are read top to bottom and the last matching rule wins (unless it says quick).
- ipfw: every rule has a number, rules are checked in number order, and the first matching rule wins, and that is the end of it.
In ipfw, the numbers are the whole story. Everything on this page was run for real on FreeBSD 15.0-RELEASE in a virtual machine, with the web server of the jails page as the target and the laptop that runs the virtual machine as the "outside" machine (the same test setup as the pf page, with port 18080 forwarded to the web server).
Step 1: pf off, ipfw loaded, safely
root@practice:~ # pfctl -d pf disabled root@practice:~ # sysrc pf_enable=NO pflog_enable=NO pf_enable: YES -> NO pflog_enable: YES -> NO
pfctl -d disables pf now, and the sysrc line stops it starting at boot.
root@practice:~ # kldstat | grep ipfw root@practice:~ # ipfw list ipfw: retrieving config failed: Protocol not available root@practice:~ # kenv net.inet.ip.fw.default_to_accept=1 net.inet.ip.fw.default_to_accept="1" root@practice:~ # kldload ipfw root@practice:~ # kldstat | grep ipfw 11 1 0xffffffff8309a000 26570 ipfw.ko root@practice:~ # ipfw list 65535 count ip from any to any not // orphaned dynamic states counter 65535 allow ip from any to any root@practice:~ # sysctl net.inet.ip.fw.default_to_accept net.inet.ip.fw.one_pass net.inet.ip.fw.verbose net.inet.ip.fw.default_to_accept: 1 net.inet.ip.fw.one_pass: 1 net.inet.ip.fw.verbose: 0 laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 404 Not Found
- kldstat shows ipfw is not loaded, and ipfw list says retrieving config failed: Protocol not available. Without the module there is no firewall to ask.
- The lockout trap. When the ipfw module is loaded, its built-in last rule is "deny everything" (unless the kernel was built the other way). Load it on a remote machine with no rules, and every packet is dropped, including your SSH session. This is the most famous way to lock yourself out of a FreeBSD server (from the documentation, not run here).
- So we set net.inet.ip.fw.default_to_accept=1 first, with kenv. It changes the built-in last rule to "allow everything". Then kldload ipfw. ipfw list shows the built-in rule number 65535: allow ip from any to any. The line above it is a bookkeeping rule for orphaned dynamic states, which you can ignore.
- The sysctl line confirms the setting. one_pass and verbose are two other switches you will meet below.
- We checked that the outside machine still got the web page and our SSH session was still alive.
A rule, taken apart
An ipfw rule looks like this:
ipfw add NUMBER ACTION PROTOCOL from SOURCE to DESTINATION [OPTIONS]
- NUMBER: the position in the list. Rules run from low to high. Gaps are normal (100, 200, 300) so you can slot rules in later.
- ACTION: allow, deny (drop silently), reset (send a TCP reset), unreach, count (just count, then carry on), pipe and queue (traffic shaping), and more (see the end of the page).
- me: a keyword meaning "any address of this machine", including the address that a jail uses.
- Options: in or out (direction), via vtnet0 (which interface), setup (a new TCP connection, the SYN), established (an existing TCP connection), keep-state (remember this connection and allow the replies), check-state (look for a remembered connection), log (write to the log).
A default-deny ruleset
root@practice:~ # ipfw -q add 100 allow ip from any to any via lo0 root@practice:~ # ipfw -q add 150 check-state root@practice:~ # ipfw -q add 200 allow tcp from any to any established root@practice:~ # ipfw -q add 250 allow ip from me to any out via vtnet0 keep-state root@practice:~ # ipfw -q add 300 allow tcp from any to me 22 in via vtnet0 setup keep-state root@practice:~ # ipfw -q add 400 allow tcp from any to me 80 in via vtnet0 setup keep-state root@practice:~ # ipfw -q add 65000 deny ip from any to any root@practice:~ # ipfw list 00100 allow ip from any to any via lo0 00150 check-state :default 00200 allow tcp from any to any established 00250 allow ip from me to any out via vtnet0 keep-state :default 00300 allow tcp from any to me 22 in via vtnet0 setup keep-state :default 00400 allow tcp from any to me 80 in via vtnet0 setup keep-state :default 65000 deny ip from any to any 65535 count ip from any to any not // orphaned dynamic states counter 65535 allow ip from any to any
Reading the seven rules, in order:
- 100: everything on the loopback is allowed, so the machine can talk to itself.
- 150, check-state: if a packet belongs to a connection that a keep-state rule has remembered, allow it right here.
- 200: any packet of an already established TCP connection is allowed. This matters: it is what keeps our own SSH session alive while we add the rules, because that connection began before the firewall had any rules.
- 250: anything this machine sends out through vtnet0 is allowed, and remembered (keep-state), so replies come back in.
- 300: SSH may come in (setup: new connections only).
- 400: web traffic may come in.
- 65000: deny everything else. This is our default-deny. We placed it well below the built-in 65535, which is still "allow" here.
ipfw list prints the rules in order. (:default is just the name ipfw gives to the connection state it remembers.) Compare what we saw with no rules, for a port with nothing listening (the forwarded port 18081):
laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18081; echo "exit code: $?" real 0m0.010s user 0m0.002s sys 0m0.008s exit code: 56
The machine's own network stack answered at once: 0.01 seconds, exit code 56 (the connection was refused). Now with the ruleset:
laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 404 Not Found laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18081; echo "exit code: $?" real 0m5.017s user 0m0.003s sys 0m0.012s exit code: 28 root@practice:~ # ipfw -a list 00100 0 0 allow ip from any to any via lo0 00150 0 0 check-state :default 00200 696 44828 allow tcp from any to any established 00250 0 0 allow ip from me to any out via vtnet0 keep-state :default 00300 0 0 allow tcp from any to me 22 in via vtnet0 setup keep-state :default 00400 9 602 allow tcp from any to me 80 in via vtnet0 setup keep-state :default 65000 1 44 deny ip from any to any 65535 0 0 count ip from any to any not // orphaned dynamic states counter 65535 401 34708 allow ip from any to any
- Port 18080 (allowed in rule 400) still gives HTTP/1.1 404 Not Found, the web server's answer.
- Port 18081 (not allowed anywhere) now takes the full 5 seconds and ends with exit code 28 (timed out). The packet fell through to deny at 65000 and was silently dropped, so the sender just waits. A closed door that is also invisible.
- ipfw -a list shows every rule with its counters: packets and bytes. Look at what did the work: rule 200 matched 696 packets (our own SSH session and the web connections' later packets), rule 400 matched 9 (the new web connections), and rule 65000 caught 1 packet (the blocked attempt on 18081). The 401 packets at the bottom were presumably counted by the "allow all" before our rules existed (counters run from the moment the module was loaded). Counters are the way to see which rule is really in charge.
First match wins
Now the lesson that makes ipfw different. Rule 400 allows web traffic. What happens if we add a deny with a number below 400, and what if the number is above?
root@practice:~ # ipfw add 350 deny tcp from any to me 80 in via vtnet0 00350 deny tcp from any to me 80 in via vtnet0 laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18080; echo "exit code: $?" real 0m5.020s user 0m0.006s sys 0m0.012s exit code: 28 root@practice:~ # ipfw delete 350 root@practice:~ # ipfw add 350 reset tcp from any to me 80 in via vtnet0 00350 reset tcp from any to me 80 in via vtnet0 laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18080; echo "exit code: $?" real 0m0.017s user 0m0.003s sys 0m0.014s exit code: 56 root@practice:~ # ipfw delete 350 laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 404 Not Found
- ipfw add 350 deny tcp from any to me 80 in via vtnet0 sits before the allow at 400. The outside machine now waits the full 5 seconds (exit 28): rule 350 matched first, and that was that. (add without -q prints the rule it made.)
- ipfw delete 350 removes it. We replaced it with reset, which answers with a TCP reset instead of dropping the packet. The failure is instant, 0.017 seconds, exit code 56. Same choice as block drop and block return in pf: silent for the internet, helpful for your own network.
- With rule 350 deleted, the page is back.
root@practice:~ # ipfw add 450 deny tcp from any to me 80 in via vtnet0 00450 deny tcp from any to me 80 in via vtnet0 laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 404 Not Found root@practice:~ # ipfw -a list | grep -E "^00(400|450)" 00400 38 2488 allow tcp from any to me 80 in via vtnet0 setup keep-state :default 00450 0 0 deny tcp from any to me 80 in via vtnet0 root@practice:~ # ipfw delete 450
Now a deny after the allow: ipfw add 450 deny .... The page still loads, because rule 400 was matched first and the packet never got as far as 450. The counters prove it: rule 400 matched 38 packets, rule 450 matched none. In pf you would put a rule later to override an earlier one. In ipfw, a rule that is too late is dead code. If a rule seems to be ignored, look for a lower-numbered rule that matches first.
Tables: ban an address without touching the rules
root@practice:~ # ipfw table badguys create root@practice:~ # ipfw table badguys add 10.0.2.2 added: 10.0.2.2/32 0 root@practice:~ # ipfw table badguys list 10.0.2.2/32 0 root@practice:~ # ipfw add 340 deny tcp from "table(badguys)" to me 80 in via vtnet0 00340 deny tcp from table(badguys) to me 80 in via vtnet0 laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18080; echo "exit code: $?" real 0m5.019s user 0m0.007s sys 0m0.011s exit code: 28 root@practice:~ # ipfw table badguys delete 10.0.2.2 deleted: 10.0.2.2/32 0 root@practice:~ # ipfw table badguys list laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 404 Not Found root@practice:~ # ipfw table all list --- table(badguys), set(0) ---
- ipfw table badguys create makes an empty table, and ipfw table badguys add 10.0.2.2 puts the outside machine's address in it. ipfw table badguys list shows what is in it.
- ipfw add 340 deny tcp from "table(badguys)" to me 80 ... is one rule that applies to every address in the table. (The quotes keep the shell from eating the parentheses.) The number 340 is below 400, so it is checked first.
- The outside machine's curl now times out (28). Our SSH session was not affected, because the rule covers only port 80.
- ipfw table badguys delete 10.0.2.2 took the address out again, with no change to the rules, and the page loaded at once. ipfw table all list lists all the tables (here the one we made, now empty).
Logging
root@practice:~ # ipfw delete 340 root@practice:~ # ipfw add 340 deny log tcp from "table(badguys)" to me 80 in via vtnet0 00340 deny log tcp from table(badguys) to me 80 in via vtnet0 root@practice:~ # sysctl net.inet.ip.fw.verbose=1 net.inet.ip.fw.verbose: 0 -> 1 root@practice:~ # ipfw table badguys add 10.0.2.2 added: 10.0.2.2/32 0 laptop:~ $ curl -sI -m 3 http://127.0.0.1:18080; echo "exit code: $?" exit code: 28 root@practice:~ # tail -3 /var/log/security Oct 5 21:00:24 practice newsyslog[1048]: logfile first created Oct 6 14:33:45 practice kernel: ipfw: 340 Deny TCP 10.0.2.2:59092 10.0.2.50:80 in via vtnet0 root@practice:~ # ipfw table badguys delete 10.0.2.2 deleted: 10.0.2.2/32 0 root@practice:~ # ipfw -a list | grep "^00340" 00340 1 44 deny log tcp from table(badguys) to me 80 in via vtnet0
Add the word log to a rule and ipfw writes a line for every packet that matches, to the system log, in /var/log/security. Two things to know: logging also needs the setting net.inet.ip.fw.verbose=1 (it was 0), and each logged rule has a limit on how many lines it writes (from the documentation, not run here). The line says what we want to know: the time, ipfw: 340 Deny TCP 10.0.2.2:59092 10.0.2.50:80 in via vtnet0: rule number, action, source address and port, destination, direction and interface. The counter of rule 340 shows it caught 1 packet. (Unlike pf, there is no separate log device: the lines go through syslogd like everything else.)
Traffic shaping with dummynet
This is the feature that makes many people choose ipfw. dummynet can limit the bandwidth of traffic, delay it, and even drop a percentage of it, all with simple rules. We made a 300 KB file on the web server and downloaded it from the outside, for the baseline:
root@practice:~ # mkdir -p /jails/web/usr/local/www/nginx-dist
root@practice:~ # head -c 300000 /dev/random > /jails/web/usr/local/www/nginx-dist/test.bin
root@practice:~ # ls -l /jails/web/usr/local/www/nginx-dist/test.bin
-rw-r--r-- 1 root wheel 300000 Oct 6 14:35 /jails/web/usr/local/www/nginx-dist/test.bin
laptop:~ $ curl -s -o /dev/null -w "%{size_download} bytes in %{time_total} s (%{speed_download} bytes/s)\n" http://127.0.0.1:18080/test.bin
300000 bytes in 0.008031 s (37355248 bytes/s)
(The jail's web root is a link to a folder that does not exist, so we made that folder first: a leftover from the poudriere page, where we switched the sample pages off.) The download takes 0.008 seconds. Now we shape it:
root@practice:~ # kldload dummynet
root@practice:~ # ipfw pipe 1 config bw 400Kbit/s
root@practice:~ # kldstat | grep dummynet
12 1 0xffffffff830c1000 128a0 dummynet.ko
root@practice:~ # ipfw pipe 1 show
00001: 400.000 Kbit/s 0 ms burst 0
q131073 50 sl. 0 flows (1 buckets) sched 65537 weight 0 lmax 0 pri 0 droptail
sched 65537 type FIFO flags 0x0 0 buckets 0 active
root@practice:~ # ipfw add 360 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
00360 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
laptop:~ $ curl -s -o /dev/null -w "%{size_download} bytes in %{time_total} s (%{speed_download} bytes/s)\n" http://127.0.0.1:18080/test.bin
300000 bytes in 0.006313 s (47520988 bytes/s)
root@practice:~ # ipfw -a list | grep "^00360"
00360 0 0 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
- kldload dummynet loads the traffic shaper. (Our first attempt, without it, failed with setsockopt(IP_DUMMYNET3): Protocol not available.)
- ipfw pipe 1 config bw 400Kbit/s creates pipe number 1, a "pipe" that only lets 400 kilobits per second through. ipfw pipe 1 show shows its settings.
- ipfw add 360 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0 sends the web server's replies through the pipe.
And nothing happened: the download took 0.006 seconds, and the counter of rule 360 says 0 packets. Why? Because of first match wins: rules 150, 200 and 250 already allowed those packets, long before rule 360. The pipe rule must come before them:
root@practice:~ # ipfw delete 360
root@practice:~ # ipfw add 120 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
00120 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
laptop:~ $ curl -s -o /dev/null -w "%{size_download} bytes in %{time_total} s (%{speed_download} bytes/s)\n" http://127.0.0.1:18080/test.bin
300000 bytes in 6.175936 s (48575 bytes/s)
root@practice:~ # ipfw -a list | grep "^00120"
00120 209 308622 pipe 1 ip from 10.0.2.50 80 to any out via vtnet0
root@practice:~ # ipfw pipe 1 show
00001: 400.000 Kbit/s 0 ms burst 0
q131073 50 sl. 0 flows (1 buckets) sched 65537 weight 0 lmax 0 pri 0 droptail
sched 65537 type FIFO flags 0x0 0 buckets 1 active
BKT Prot ___Source IP/port____ ____Dest. IP/port____ Tot_pkt/bytes Pkt/Byte Drp
0 ip 0.0.0.0/0 0.0.0.0/0 209 308622 0 0 0
root@practice:~ # ipfw delete 120
root@practice:~ # ipfw pipe 1 delete
laptop:~ $ curl -s -o /dev/null -w "%{size_download} bytes in %{time_total} s (%{speed_download} bytes/s)\n" http://127.0.0.1:18080/test.bin
300000 bytes in 0.007804 s (38441824 bytes/s)
We deleted rule 360 and added the same rule as number 120. Now the 300 KB download took 6.18 seconds, which is 48,575 bytes per second, exactly the 400 kilobits that the pipe allows. The counter shows 209 packets (308,622 bytes) went through rule 120, and ipfw pipe 1 show lists the traffic that went through the pipe. Deleting the rule and the pipe brings the speed back to full (0.008 seconds in the last line). That is how you test an application over a slow link, or protect a network from one greedy download. Dummynet can also add delay (delay 100ms) and random packet loss (plr 0.05) to a pipe, and share bandwidth fairly between flows with queues (from the documentation, not run here).
Making it permanent
Rules typed at the prompt vanish at the next reboot. Put them in a script and tell the system to run it at boot:
root@practice:~ # cat > /etc/ipfw.rules <<'EOF' > #!/bin/sh > # TechCommune lab ruleset: default deny, SSH and HTTP in > cmd="ipfw -q add" > ipfw -q -f flush > $cmd 100 allow ip from any to any via lo0 > $cmd 150 check-state > $cmd 200 allow tcp from any to any established > $cmd 250 allow ip from me to any out via vtnet0 keep-state > $cmd 300 allow tcp from any to me 22 in via vtnet0 setup keep-state > $cmd 400 allow tcp from any to me 80 in via vtnet0 setup keep-state > $cmd 65000 deny ip from any to any > EOF root@practice:~ # sh -n /etc/ipfw.rules && echo syntax-ok syntax-ok root@practice:~ # sysrc firewall_enable=YES firewall_script=/etc/ipfw.rules firewall_logging=YES firewall_enable: NO -> YES firewall_script: /etc/rc.firewall -> /etc/ipfw.rules firewall_logging: NO -> YES root@practice:~ # grep "^firewall" /etc/rc.conf firewall_enable="YES" firewall_script="/etc/ipfw.rules" firewall_logging="YES" root@practice:~ # service ipfw restart 2>&1 | tail -4 Firewall rules loaded. Firewall logging enabled. root@practice:~ # ipfw list 00100 allow ip from any to any via lo0 00150 check-state :default 00200 allow tcp from any to any established 00250 allow ip from me to any out via vtnet0 keep-state :default 00300 allow tcp from any to me 22 in via vtnet0 setup keep-state :default 00400 allow tcp from any to me 80 in via vtnet0 setup keep-state :default 65000 deny ip from any to any 65535 count ip from any to any not // orphaned dynamic states counter 65535 allow ip from any to any
- The script starts with ipfw -q -f flush to clear the old rules, then adds the same seven rules. A variable, cmd, saves typing.
- sh -n checks the script's syntax without running it. Always do this first.
- Three lines in /etc/rc.conf do the rest: firewall_enable="YES" starts the firewall at boot, firewall_script names our script (the default is the stock /etc/rc.firewall, with its ready-made firewall_type choices: open, client, simple, closed and workstation: from the documentation, not run here), and firewall_logging="YES" switches on logging.
- service ipfw restart loaded the script (Firewall rules loaded.) and the list is as before. The web page was reachable (it says 403 now: we made the folder, but it has no home page).
Then the real test, a reboot, with the rules coming from the script:
root@practice:~ # uptime 2:36PM up 20 secs, 1 user, load averages: 0.28, 0.06, 0.02 root@practice:~ # kldstat | grep -E "ipfw|dummynet" 9 1 0xffffffff83033000 26570 ipfw.ko root@practice:~ # ipfw list 00100 allow ip from any to any via lo0 00150 check-state :default 00200 allow tcp from any to any established 00250 allow ip from me to any out via vtnet0 keep-state :default 00300 allow tcp from any to me 22 in via vtnet0 setup keep-state :default 00400 allow tcp from any to me 80 in via vtnet0 setup keep-state :default 65000 deny ip from any to any 65535 count ip from any to any not // orphaned dynamic states counter 65535 deny ip from any to any root@practice:~ # sysctl net.inet.ip.fw.default_to_accept net.inet.ip.fw.verbose net.inet.ip.fw.default_to_accept: 0 net.inet.ip.fw.verbose: 1 root@practice:~ # service ipfw status ipfw is enabled
The machine was back and reachable over SSH about 15 seconds after the reboot. uptime says 20 seconds, ipfw.ko was loaded by the startup scripts, and the rules are all there. Look at the last line: this time the built-in rule 65535 says deny, and default_to_accept is 0. At boot the module is loaded with the kernel's own default, which is deny, and our script then adds the rules that let us in. Between those two moments, everything is blocked (from the documentation, not run here), which is why the script must let SSH in. (firewall_logging also set verbose to 1.)
laptop:~ $ curl -sI -m 5 http://127.0.0.1:18080 | head -1 HTTP/1.1 403 Forbidden laptop:~ $ time curl -sI -m 5 http://127.0.0.1:18081; echo "exit code: $?" real 0m5.019s user 0m0.006s sys 0m0.010s exit code: 28
And from the outside: port 18080 answers (403), and the unlisted port 18081 times out (exit 28 after 5 seconds). Same behaviour as before the reboot.
What we did not do
All from the documentation, not run here:
- NAT: ipfw nat is a NAT engine in the kernel. Together with gateway_enable it lets a FreeBSD machine share one internet connection.
- Redirects and routing tricks: fwd (send a packet to another next hop), divert (hand packets to a program), skipto (jump to a rule number, to build structure).
- Sets: ipfw set groups rules so that you can switch them on and off together.
- Per-jail rules: ipfw add allow tcp from any to any jail 1.
- Presets: the ready-made firewall_type rulesets in /etc/rc.firewall.
ipfw or pf?
- Rule order: ipfw, first match wins, numbered. pf, last match wins (unless quick), in file order.
- State: ipfw, you ask for it with keep-state. pf keeps state by default for pass rules.
- Shaping: ipfw has dummynet. pf has ALTQ, which our default kernel did not include.
- NAT: both have it. In pf it is part of the rules file. In ipfw it is a separate nat object.
- Logging: ipfw uses syslog (/var/log/security). pf uses a log device (pflog0) that you read with tcpdump.
- Where you will meet them: pf is the one behind pfSense and OPNsense and is the more popular choice for servers. ipfw is the choice if you want built-in traffic shaping or like numbered rules.
- If you come from Linux: both are in the same family as iptables and nftables. ipfw's numbered rules and first-match order will look the most familiar, and ufw's "allow/deny" words are close to ipfw's actions.
Whichever you choose, choose one, and learn to test a ruleset the way we did: from the outside, with a way back in.
⚠️ Never enable ipfw on a remote machine without a way back in. Set net.inet.ip.fw.default_to_accept=1 before loading the module, put an SSH rule in before the final deny, rule established in, check the script with sh -n, and keep a second SSH session or the console open when you change anything.
It came after the one that said "allow everything".<br>
It is very first-come, first-served in there.
🧪 Try it yourself
On a FreeBSD virtual machine (never first on a production server), with a service to test:
1. Turn pf off (pfctl -d), set net.inet.ip.fw.default_to_accept=1 with kenv, and load ipfw. What does ipfw list show? 2. Add a default-deny ruleset: loopback, check-state, established, outgoing with keep-state, SSH, your service, and a deny at 65000. 3. From another machine, test the service and a closed port. How long does each take to answer? 4. Put a deny before your allow, then after it. Which one works? Check ipfw -a list. 5. Change deny to reset. What changes for the machine that is blocked? 6. Make a table, add the other machine's address, and ban it from your service. Remove it again without touching the rules. 7. Load dummynet, make a pipe of 400 Kbit/s, and shape a download. Put the pipe rule first at a number that is too high, then a number that works. 8. Write the rules into a script, check it with sh -n, set the firewall_* lines in rc.conf, and reboot.
The cheat sheet
- Load safely: kenv net.inet.ip.fw.default_to_accept=1, then kldload ipfw.
- Rules: ipfw add NUMBER allow|deny|reset|count|pipe ... from SRC to DST [in|out] [via IF] [setup|established] [keep-state] [log], ipfw list, ipfw -a list (counters), ipfw delete NUMBER, ipfw -f flush.
- Order: lowest number first, first match wins. check-state early.
- Tables: ipfw table NAME create, ipfw table NAME add|delete ADDRESS, ipfw table NAME list, rule from "table(NAME)".
- Log: log in the rule, sysctl net.inet.ip.fw.verbose=1, lines in /var/log/security.
- Shaping: kldload dummynet, ipfw pipe 1 config bw 400Kbit/s, ipfw add 120 pipe 1 ip from ... out. Put it early.
- Permanent: script + sysrc firewall_enable=YES firewall_script=/etc/ipfw.rules firewall_logging=YES, sh -n, service ipfw restart.
- Escape hatches: ipfw -f flush (back to the default rule), kldunload ipfw (removes the firewall), and the console.
← vm-bhyve: Virtual Machines with Short Commands · ↑ FreeBSD Corner · Linux Binary Compatibility: Running Linux Programs on FreeBSD →