The morning after a break-in attempt is when most administrators meet their firewall for the second time. The first time was the day the server was built: ufw default deny incoming, ufw allow OpenSSH, ufw allow 'Nginx Full', ufw enable, and a reassuring line about the firewall being active and enabled on system startup. Now somebody wants to know what the machine actually lets through, and ufw status answers with three rules, which is the number of rules the administrator wrote. It is not the number of rules the kernel is running. For that there is nft list ruleset, and the answer to that runs to several screens, most of it in a language the administrator was promised he would never have to read.
This piece counts what is on those screens, and then does the same job on FreeBSD.
The habit
Ubuntu's server documentation says it plainly: "The default firewall configuration tool for Ubuntu is ufw. Developed to ease iptables firewall configuration, ufw provides a user-friendly way to create an IPv4 or IPv6 host-based firewall." Jamie Strandboge wrote it at Canonical in 2008, and its README still carries his view on the name: it "has come to mean 'Uncomplicated Firewall', but you can change it to something more suitable if you want … If you are not a fan, perhaps 'Unbearable Firewall'. Have fun!" It does what it set out to do. The defaults in its configuration file drop incoming traffic, accept outgoing, drop forwarding and switch IPv6 on, and ufw limit 22/tcp adds a brute-force brake that refuses an address after six connection attempts in thirty seconds. Red Hat's side of the world does the same job with firewalld, which arranges interfaces into zones and services into names, so that firewall-cmd --add-service=ssh is the whole conversation.
The pride in both is the same pride: the administrator writes a policy and never touches the rule language underneath. That convenience is real, and it has a price that the status command does not print.
What the kernel actually holds
ufw is a Python program, and its rule files are checked into its repository on Launchpad, where I counted them on 20 October 2026. When ufw is enabled it hands four of those files to iptables-restore before it looks at a single rule of yours: before.rules with 24 rules for IPv4, before6.rules with 65 for IPv6, after.rules with 7 and after6.rules with 6. That is 102 rules, and the limit machinery in user.rules adds three more (I count them, since ufw loads them whether or not you ever type limit), so the hundred-odd rules are in place before ufw allow 22 writes its first line. They sit in 21 chains that the files declare, and the backend keeps another 22 for logging, eleven per address family. The README is candid about it: "There is some default configuration in both 'before.rules' and 'after.rules', and this configuration is not displayed with 'ufw status' (but can always be viewed with 'iptables -L -n' …)."
What the 24 IPv4 rules do is nothing exotic. Two accept loopback. Three accept packets belonging to established connections. Two log and drop packets that connection tracking calls invalid. Eight accept four ICMP types, on input and on forward. One lets a DHCP answer in. Six handle traffic to addresses that are not local, with a rate-limited log. Two open the ports for mDNS and UPnP discovery, on a server, which is a decision one may want to revisit. The IPv6 file is the same idea with neighbour discovery and router advertisements added, which is how it comes to 65.
Then there is the question of who speaks to the kernel. ufw writes iptables syntax, the syntax of a tool that Debian has considered superseded since 2019. The Debian wiki puts it in one sentence: "nftables has been the default framework in Debian since DebianBuster", and the next: "Even if iptables is installed, it actually uses nftables via a iptables-nft compatibility layer." Ubuntu inherits the arrangement. So the path from the three lines you typed to the packet filter runs through ufw, which writes iptables rules, which iptables-nft translates into nftables objects, which the nf_tables subsystem in the kernel finally evaluates. That makes three translators and two rule languages, with a status command that reports on the first of them. nft list ruleset shows the end of the chain, in the third language, with chain names like ufw-before-logging-input and ufw-skip-to-policy-output that nobody chose to learn.
firewalld takes a different route to a similar place. Its nftables backend, the default since firewalld 0.6.0 in 2018, builds its own table, inet firewalld, and gives every zone seven base chains, each with five sub-chains called _pre, _log, _deny, _allow and _post. A ruleset posted from a fresh Fedora installation with nothing added shows 75 chains and around 130 rule lines in that table, 42 of the chains carrying the name of the one active zone. The policy the administrator wrote was, once again, one line.
The same job on FreeBSD
The whole policy, in /etc/pf.conf:
set skip on lo0
block in
pass out
pass in on egress proto tcp to port { 22 80 443 }
Two lines in /etc/rc.conf, pf_enable="YES" and pflog_enable="YES", and service pf start. The loopback exemption is the first line. Established connections are allowed because every pass rule in pf keeps state unless told not to; pf.conf(5) offers no state for the rare occasion when one does not want that. The brute-force brake, should one want it, is a clause on the same line: keep state (max-src-conn-rate 6/30, overload <bruteforce> flush global). And if the machine is to answer ping, that is a fifth line, pass in inet proto icmp icmp-type echoreq: a server that is meant to be pinged says so in its firewall, and one that is not says nothing, which is the whole of the policy either way.
What the kernel receives from those four lines is written down by the project itself. pf.conf(5) says that certain parameters "can be expressed as lists, in which case pfctl(8) generates all needed rule combinations", and the pfctl regression tests in sbin/pfctl/tests of FreeBSD 15.1, 149 input files run through pfctl -nv and compared against their expected output, show the arithmetic. Test 0024 feeds in pass in proto tcp from any to any port { ssh ftp } and expects two rules back, each ending in flags S/SA keep state; test 0001 feeds in pass in all and expects pass in all flags S/SA keep state. By those rules the four lines above become one option and six rules: the block, the pass out, and three pass in rules, one per port, each stateful. pfctl -sr prints those six, because those six are what is loaded. There is no second view.
# from sbin/pfctl/tests/files, FreeBSD 15.1
pf0024.in: pass in proto tcp from any to any port { ssh ftp }
pf0024.ok: pass in proto tcp from any to any port = ssh flags S/SA keep state
pass in proto tcp from any to any port = ftp flags S/SA keep state
pf0001.in: pass in all
pf0001.ok: pass in all flags S/SA keep state
And there is no second language. pfctl parses pf.conf and hands the result to pf(4) over one interface, both of them in the same source tree as the kernel, released on the same day. The file you wrote is the ruleset the kernel runs, to the comma.
The difference, shown
Counted on 20 October 2026 from the ufw rule files on Launchpad and the pfctl test suite in FreeBSD 15.1; the firewalld figures above come from a published ruleset and are marked as such.
The last row is the one to read. A firewall is a thing one reads under pressure, at the moment the question is what does this machine let in, and on FreeBSD the honest answer is the file. On Ubuntu the honest answer is a ruleset the administrator has never seen, in a syntax the distribution calls a compatibility layer, with a status command that shows the part he wrote and omits the part he did not.
The limit
Most of the hundred are housekeeping. Loopback, established connections, ICMP errors, DHCP and neighbour discovery are housekeeping a careful administrator does anyway; ufw writes them for every machine, unseen. On FreeBSD they are three or four lines of your own, where you can see them.
Written by hand, nftables is also one file. A hand-written /etc/nftables.conf for the same policy is one table with one chain and a handful of rules, and whoever writes it gets the single view. The distributions chose the layers on his behalf, and the count above is of the defaults, because the defaults are what most machines run.
nftables has constructs of its own. Maps that pick a verdict from a packet field and flowtables that hand established flows to a fast path have no one-to-one counterpart in pf; pf meets most of the same needs with tables, anchors, macros and a state engine that has carried the OpenBSD and FreeBSD firewalls for a quarter of a century.
firewalld solves a laptop problem. Zones exist so that a machine can move between networks and carry different rules to each. A server does not move, and the comparison here is between servers.
No speed was measured. Netfilter and pf both filter at line rate for anything a web server will see, and nothing here claims otherwise.
Strandboge offered 'Unbearable Firewall' as the alternative reading of the name. On the morning after, with nft list ruleset scrolling past, his second reading is the one that comes to mind.
The four lines of pf.conf are still four lines, and the kernel has read the same ones.