Thuta Learning
AdvancedDevOps & Toolsbeginner

Firewalls and Packet Filtering

What you'll walk away with

  • Explain the core ideas behind Firewalls and Packet Filtering
  • Read the diagram and trace how data actually moves through it
  • Run the sample code and verify its output

Build the mental model

A firewall's job sounds simple, decide whether a packet may pass, but the design turns on how much it remembers.

A stateless filter examines each packet in isolation, using only the header fields in front of it: source and destination address, protocol, ports, flags. It has no memory of what came before. That is cheap and fast, and it creates an ugly problem. When your server makes an outbound request, the reply arrives from an arbitrary remote port back to a high-numbered ephemeral port on your machine. A stateless filter that wants to allow those replies cannot distinguish them from unsolicited inbound connections to those same ports, so it ends up opening the entire ephemeral range to the world.

A stateful firewall keeps a connection table. When it permits an outbound flow it records the five-tuple, and return packets matching an existing entry are accepted because they belong to a conversation you started, not because a port was left open. This is why the first rule in almost every real ruleset accepts ESTABLISHED traffic. The distinction is invisible in the packet itself: two packets can be byte-identical and get opposite verdicts purely because one belongs to a known flow.

What makes any of this work is default deny. The last rule drops everything, and each rule above it carves out one specific exception. The alternative, allowing everything and blocklisting the bad, fails the moment someone deploys a service you did not anticipate.

Rules are evaluated top to bottom and the first match wins. Ordering is the program. A broad allow placed above a narrow deny silently makes the deny unreachable, and nothing warns you.

Finally, a network firewall filters at a boundary; a host firewall runs on the machine itself and still applies to traffic that never crossed that boundary.

text
A PACKET THROUGH AN ORDERED RULE CHAIN
--------------------------------------
          packet arrives
                |
                v
   +-----------------------------------+
   | 1. state == ESTABLISHED ?         |-- match --> ACCEPT
   +-----------------------------------+
                | no match
                v
   +-----------------------------------+
   | 2. dst 203.0.113.10:443, NEW ?    |-- match --> ACCEPT
   +-----------------------------------+
                | no match
                v
   +-----------------------------------+
   | 3. src 10.0.0.0/24 -> :22, NEW ?  |-- match --> ACCEPT
   +-----------------------------------+
                | no match
                v
   +-----------------------------------+
   | 4. default policy                 |------------> DROP
   +-----------------------------------+

FIRST MATCH WINS
  Nothing below a matching rule is ever read. Put a broad ACCEPT
  above a narrow DROP and the DROP becomes dead code, silently.

WHY RULE 1 EXISTS
  stateless:  to let replies in you must open the whole
              ephemeral range 32768-60999 to the internet
  stateful:   the reply matches a flow YOU started, so no
              inbound port is opened at all

Connect it to a real scenario

Here is a rule-ordering bug that ships to production constantly. Someone exposes a new admin panel, adding an allow rule for port 8080 from anywhere. Later, a security review adds a deny rule for that panel from outside the office, placed at the bottom near the other denies. It never fires: the allow above it already matched, and evaluation stopped there. The ruleset reads correctly to a human and does the wrong thing.

The habit that prevents this: order rules most specific to least specific, and treat the final default-deny as the only broad rule in the chain. Any allow saying from anywhere should prompt you to check whether something narrower belongs above it.

The second habit is to choose between reject and drop deliberately. A reject sends an ICMP message or TCP RST, so the client fails immediately. A drop sends nothing, so the client waits for a timeout. Drop is better facing the internet, because it gives a scanner no information at all. Reject is better internally: a thirty-second hang is a far worse debugging experience than an instant refusal, and that timeout-versus-refused distinction is exactly what tells you a firewall is involved.

The engine below evaluates a fixed set of packets against an ordered chain; its two port 54321 packets show statefulness deciding the outcome by itself.

Try the working example

python
import ipaddress

# An ordered rule chain. Order is the whole program: the first rule that
# matches decides, and nothing below it is ever consulted.
RULES = [
    {"n": 1, "action": "ACCEPT", "state": "ESTABLISHED", "src": "any",
     "dst": "any", "port": "any"},
    {"n": 2, "action": "ACCEPT", "state": "NEW", "src": "any",
     "dst": "203.0.113.10", "port": 443},
    {"n": 3, "action": "ACCEPT", "state": "NEW", "src": "10.0.0.0/24",
     "dst": "203.0.113.10", "port": 22},
    {"n": 4, "action": "DROP", "state": "any", "src": "any",
     "dst": "any", "port": "any"},          # default deny, last
]


def matches(rule, pkt):
    if rule["state"] != "any" and rule["state"] != pkt["state"]:
        return False
    if rule["src"] != "any":
        if ipaddress.ip_address(pkt["src"]) not in ipaddress.ip_network(rule["src"]):
            return False
    if rule["dst"] != "any" and rule["dst"] != pkt["dst"]:
        return False
    if rule["port"] != "any" and rule["port"] != pkt["port"]:
        return False
    return True


def evaluate(pkt):
    for rule in RULES:
        if matches(rule, pkt):
            return rule["n"], rule["action"]
    return None, "DROP"   # unreachable here; rule 4 always matches


PACKETS = [
    {"src": "198.51.100.7", "dst": "203.0.113.10", "port": 443, "state": "NEW"},
    {"src": "198.51.100.7", "dst": "203.0.113.10", "port": 22, "state": "NEW"},
    {"src": "10.0.0.5", "dst": "203.0.113.10", "port": 22, "state": "NEW"},
    {"src": "93.184.216.34", "dst": "203.0.113.10", "port": 54321,
     "state": "ESTABLISHED"},
    {"src": "93.184.216.34", "dst": "203.0.113.10", "port": 54321,
     "state": "NEW"},
    {"src": "198.51.100.7", "dst": "203.0.113.10", "port": 3306, "state": "NEW"},
]

for pkt in PACKETS:
    n, action = evaluate(pkt)
    where = "rule " + str(n) if n else "policy"
    print(action.ljust(7) + pkt["src"].ljust(15) + " -> " +
          pkt["dst"] + ":" + str(pkt["port"]).ljust(6) +
          pkt["state"].ljust(12) + "(" + where + ")")

print("")
# The two 54321 packets are the point of the whole lesson: identical
# 5-tuples, opposite outcomes, decided purely by connection state.
print("packets 4 and 5 are byte-identical except for state.")
print("stateless filtering cannot tell them apart; stateful can.")
You should see
ACCEPT 198.51.100.7    -> 203.0.113.10:443   NEW         (rule 2)
DROP   198.51.100.7    -> 203.0.113.10:22    NEW         (rule 4)
ACCEPT 10.0.0.5        -> 203.0.113.10:22    NEW         (rule 3)
ACCEPT 93.184.216.34   -> 203.0.113.10:54321 ESTABLISHED (rule 1)
DROP   93.184.216.34   -> 203.0.113.10:54321 NEW         (rule 4)
DROP   198.51.100.7    -> 203.0.113.10:3306  NEW         (rule 4)

packets 4 and 5 are byte-identical except for state.
stateless filtering cannot tell them apart; stateful can.

5-minute try-it

Move rule 2 above rule 3 and change its port to any, then see which packets change verdict. Next delete rule 1 (the ESTABLISHED rule) and re-run, then write the rule a stateless firewall would need in order to let packet 4 through.

One important caution

Placing a broad allow rule above a narrower deny: first-match-wins makes the deny unreachable and nothing warns you.

Using DROP on internal networks: clients hang until timeout instead of failing fast, which turns a clear refusal into a slow, ambiguous outage.

Cloudflare Learning Center - What is a firewall?Computer Networking

Easy traps

  • Placing a broad allow rule above a narrower deny: first-match-wins makes the deny unreachable and nothing warns you.
  • Using DROP on internal networks: clients hang until timeout instead of failing fast, which turns a clear refusal into a slow, ambiguous outage.
  • Validate sample code in a local or test environment before applying it to a production network.

Exercise

Move rule 2 above rule 3 and change its port to any, then see which packets change verdict. Next delete rule 1 (the ESTABLISHED rule) and re-run, then write the rule a stateless firewall would need in order to let packet 4 through.

You'll know it worked when: ACCEPT 198.51.100.7 -> 203.0.113.10:443 NEW (rule 2) DROP 198.51.100.7 -> 203.0.113.10:22 NEW (rule 4) ACCEPT 10.0.0.5 -> 203.0.113.10:22 NEW (rule 3) ACCEPT 93.184.216.34 -> 203.0.113.10:54321 ESTABLISHED (rule 1) DROP 93.184.216.34 -> 203.0.113.10:54321 NEW (rule 4) DROP 198.51.100.7 -> 203.0.113.10:3306 NEW (rule 4) packets 4 and 5 are byte-identical except for state. stateless filtering cannot tell them apart; stateful can.

Firewalls and Packet Filtering | Thuta Learning