<!--date=20250206 -->


<h1>Firewall Concepts: Packet Filtering, NAT, and Stateful Inspection</h1>
<p>Before getting into specific tools like pf or nftables, it's worth being clear on what a firewall is actually doing at each layer. These concepts come up constantly when reading documentation or debugging why traffic isn't flowing the way you expect.</p>

<h2>Packet Filtering</h2>
<p>The simplest form of firewalling: look at each packet's headers and decide pass or drop based on a ruleset. Headers contain the source and destination IP addresses, the ports, and the protocol (TCP, UDP, ICMP). That's all packet filtering sees — it has no knowledge of whether this packet is part of an ongoing conversation or a fresh connection attempt.</p>
<pre>
# pf example: pure stateless packet filter rules
block all                        # default deny
pass in proto tcp to port 443    # allow HTTPS in
pass in proto tcp to port 22     # allow SSH in
pass out all                     # allow all outbound

Without 'keep state', each packet is evaluated in isolation.
Return traffic from an outbound connection would be blocked
unless you explicitly allow the inbound direction too.
</pre>
<p>Stateless packet filtering is fast because it's simple. The downside: an attacker can craft packets that look valid by header alone. And without state tracking, allowing return traffic for outbound connections requires writing explicit rules for every protocol — which gets messy fast.</p>

<h2>Stateful Filtering</h2>
<p>Stateful inspection is what every serious firewall has used since the late 90s. Instead of treating each packet independently, the firewall maintains a state table of active connections. When a packet arrives, the firewall first checks the state table: is this part of a known, legitimate connection? If yes, pass it. If not, apply the ruleset to decide whether to track a new connection.</p>
<pre>
# pf — 'keep state' creates a state table entry when the connection starts
pass in proto tcp to port 80 keep state

State table entry records:
  source IP and port
  destination IP and port
  protocol (TCP)
  TCP sequence numbers (detects out-of-sequence spoofed packets)
  connection state (SYN-SENT, ESTABLISHED, CLOSING, etc.)

Return traffic (server responses) is automatically allowed
because the state table tracks the outbound packets of the connection.
</pre>

<h3>Stateless vs. Stateful: When Each Applies</h3>

<table>
  <thead><tr><th>Context</th><th>Better approach</th><th>Reason</th></tr></thead>
  <tbody>
    <tr><td>TCP connections (HTTP, SSH, etc.)</td><td>Stateful</td><td>Connection-oriented; state tracking is natural and correct</td></tr>
    <tr><td>UDP (DNS, NTP, streaming)</td><td>Either — stateless is simpler</td><td>No connection concept; state table has to use timeouts</td></tr>
    <tr><td>High-throughput environments</td><td>Stateless for bulk, stateful for control</td><td>State table memory/CPU overhead matters at scale</td></tr>
    <tr><td>Blocking known-bad IP ranges</td><td>Stateless</td><td>Simple and fast; no need to track state for a DROP rule</td></tr>
  </tbody>
</table>

<h2>Network Address Translation (NAT)</h2>
<p>NAT rewrites IP addresses in packet headers as they pass through the firewall. The most common use: a private network (192.168.x.x or 10.x.x.x) that needs to reach the internet through a single public IP. The firewall replaces the private source address with the public one on the way out, then reverses the translation for return traffic.</p>

<table>
  <thead><tr><th>NAT type</th><th>Rewrites</th><th>Use case</th></tr></thead>
  <tbody>
    <tr><td>Source NAT (SNAT / masquerade)</td><td>Source address</td><td>Private network accessing internet — what your home router does</td></tr>
    <tr><td>Destination NAT (DNAT / port forwarding)</td><td>Destination address</td><td>Incoming traffic forwarded to an internal host</td></tr>
    <tr><td>1:1 NAT (static NAT)</td><td>Both, bidirectionally</td><td>A server that needs its own dedicated public IP</td></tr>
  </tbody>
</table>

<pre>
# pf — masquerade the internal network behind the external interface
nat on em0 from 192.168.1.0/24 to any -> (em0)

# pf — forward incoming port 80 to internal web server
rdr on em0 proto tcp to port 80 -> 192.168.1.10 port 80

# nftables — masquerade
table ip nat {
    chain postrouting {
        type nat hook postrouting priority 100;
        ip saddr 192.168.1.0/24 oifname "eth0" masquerade
    }
}
</pre>

<h2>Application-Layer Firewalls</h2>
<p>Regular packet filtering and stateful inspection operate at layers 3 and 4 of the network model — IP and TCP/UDP. Application-layer firewalls go further and inspect the actual content of traffic: the HTTP request URL, the DNS query name, the FTP command being issued.</p>
<p>This is more powerful but also more CPU-intensive and more complex:</p>
<ul>
  <li>Block specific HTTP URLs or user agents while allowing others on port 80</li>
  <li>Detect SQL injection or XSS patterns in web traffic</li>
  <li>Enforce DNS filtering or block known-bad domains</li>
  <li>Identify applications regardless of what port they use</li>
</ul>
<p>The catch with HTTPS: TLS encrypts everything above the TCP layer. Application-layer inspection of HTTPS requires the firewall to terminate the TLS session, inspect the plaintext, and re-encrypt — effectively a man-in-the-middle. This works in corporate environments with their own CA, but it breaks certificate pinning, introduces a new trust boundary, and adds significant complexity.</p>
<p>For most self-hosted setups, application-layer inspection isn't worth the complexity. A well-configured stateful firewall with fail2ban covers the vast majority of threats that a personal server or small site faces.</p>

<h2>How They Work Together</h2>

<table>
  <thead><tr><th>Technique</th><th>Network layer</th><th>Sees</th><th>Typical use</th></tr></thead>
  <tbody>
    <tr><td>Packet filtering</td><td>3 / 4</td><td>IP, port, protocol</td><td>Basic allow/deny rules; blocking bad IP ranges</td></tr>
    <tr><td>Stateful inspection</td><td>3 / 4 + state</td><td>Connection state, TCP sequence numbers</td><td>Dynamic connections; standard use for most firewalls</td></tr>
    <tr><td>NAT</td><td>3</td><td>IP addresses (rewrites them)</td><td>Private networks, port forwarding</td></tr>
    <tr><td>Application layer (WAF/DPI)</td><td>7</td><td>Protocol content</td><td>WAF, content filtering, IDS/IPS</td></tr>
  </tbody>
</table>

<p>In practice: most firewalls use stateful inspection as the core, add NAT for private networks, and leave application-layer inspection to dedicated tools — a WAF in front of web servers (nginx, Cloudflare), or an IDS like Snort/Suricata running in parallel rather than inline.</p>

<h2>References</h2>
<ul>
  <li><a href="https://www.openbsd.org/faq/pf/filter.html" target="_blank" rel="noopener">OpenBSD pf — Packet Filtering ↗</a></li>
  <li><a href="https://wiki.nftables.org" target="_blank" rel="noopener">nftables wiki ↗</a></li>
  <li>Stevens — <em>TCP/IP Illustrated, Vol. 1</em> (foundational reference for all of this)</li>
</ul>
