---
title: 'Firewall Types'
source: 'https://academia.sh/en/courses/wireless-and-security/firewall-types'
course: 'Wireless Networks and Network Security'
language: en
updated: '2026-08-17T18:07:22+00:00'
license: 'CC BY-SA 4.0'
---

# Firewall Types

The semantics of packet-filtering, stateful, and application-aware mechanisms on the same forty flows: the unmirrored filter false-admits 6 flows and false-denies 7, the stateful mechanism zeroes both, and the application-aware mechanism — because it reads a field intent never wrote — produces only 8 false denies.

The previous lesson finished cell coverage and left this behind: up to this point, what
drew the boundary was the **medium**. Where signal strength dropped below the threshold,
the link broke; nobody had written the edge of the cell. Radius was a design promise; the
party that enforced it was physics.

This lesson starts where the boundary gets written. A **firewall** does not leave the
question of where passage ends to the medium: it writes the boundary as an **ordered rule
chain** and runs every flow through that chain. A written boundary has one advantage over
the medium's: it is readable. It also has one shortcoming: what runs is whatever the
writer wrote, not what they meant. This lesson's question is why the same chain behaves
differently across different mechanisms.

## Two Rules of the Written Boundary

The rule chain's semantics are defined by two rules, and both stay fixed throughout the
course.

**First match wins.** The flow enters the chain from the top, the action of the first
matching rule is applied, and evaluation ends there. The rest of the chain is not read.

**The chain ends in implicit deny.** If no rule matches, the flow does not pass.
**Implicit deny** is not written; it is born from where the chain ends. This is the silent
source of **false denies** in measurement: the flow the implicit deny stops has no rule
name standing against it, so nobody looks for it.

Together, these two rules make **evaluation order** part of the meaning. The order itself
is a separate object of measurement and is the next lesson's subject; here the chain is
held fixed and only the mechanism running it changes. The rule dump is as follows:

```text
# taught rule dump , not executed
# the syntax is fictional and matches no device's actual format

step  action   source   dest   port
  1   permit   ext      dmz    80
  2   permit   ext      dmz    443
  3   permit   dmz      int    3306
  4   permit   mgmt     *      *
  5   permit   int      dmz    *
  6   permit   int      ext    *
  7   permit   int      int    *
  -   implicit deny : where the chain ends
```

The Perimeter Defense topic of the Cybersecurity curriculum covered the firewall by its
**generations** and its stance against attack; that narrative is not repeated here. What
this lesson measures is not generation but how **the field set the mechanism reads** turns
the same chain into a different boundary.

## What the Three Kinds See

The difference among the three kinds is not in how the rules are written, but in the
**input** the rule is applied to.

**Packet filtering** evaluates every packet on its own: it reads the source, destination,
and port, makes the decision, and forgets. It has no memory.

A **stateful** mechanism keeps a state table. When a conversation is opened and let
through, it is written to the table; that conversation's later packets are not run through
the rule chain again, they are answered from the table. The question this mechanism asks
the rule chain is not the packet's own direction, but the **direction that opened the
conversation**.

An **application-aware** mechanism reads, on top of this, what the flow actually carries:
the port implies an application, and the carried content may or may not confirm it.

Packet filtering's memorylessness has a direct consequence. The return direction of a
conversation that was let through enters the rule chain as a reversed triple and usually
matches no rule; the implicit deny cuts it off. The known static remedy for this gap is the
**mirror rule**: for every directional rule, a second copy is added to the chain with its
source and destination swapped. The mirror is an imitation of state tracking, and it
stumbles in two places. First, a rule that does not write both ends has no mirror. Second,
the mirror rule cannot tell a return packet apart from a **new** connection; it opens the
same door to both.

The measurement's assumptions:

- **NS1** — The forty flows are produced from the shared fixture; their source,
  destination, and port come from the fixture. The oracle is the intent itself, and it is
  known because we wrote the fixture.
- **NS2** — Two fields are added to each flow: **phase** (a new connection, or the
  continuation of an already-open conversation) and **carries** (whether it carries the
  application the port implies or not). Both come from a separate generator; the base
  forty flows do not change.
- **NS3** — The intent of an established flow is the intent of **the direction that opened
  the conversation**: policy is applied to the reversed triple. The policy function itself
  is not changed.
- **NS4** — All four mechanisms use **the same seven-rule chain**. The only thing that
  changes is the field set the mechanism reads; the correctness of the rule set is not
  measured in this lesson.
- **NS5** — On an established flow, the stateful mechanism applies the chain to the
  conversation's **opening direction**; this is the information the state table records.
- **NS6** — On top of the stateful decision, the application-aware mechanism checks
  whether the carried application matches the port. The oracle does **not** read this
  field.
- **NS7** — In a set of forty events, the smallest measurable difference is
  **1/40 = 0.025**; a smaller difference cannot be defended with this set.

## Measurement

```python
"""Firewall types: four mechanisms on the same forty flows.

Part 1 - the rule chain is fixed; what changes is the field set the mechanism reads.
Part 2 - each mechanism's two-way deviation from intent.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
PORTS = (80, 443, 22, 3306, 8080)


def generator(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def flows(count=40, seed=SEED):
    r, out = generator(seed), []
    for i in range(count):
        out.append({"no": i + 1, "source": ZONES[r(4)],
                    "dest": ZONES[r(4)], "port": PORTS[r(5)]})
    return out


def annotate(ak, seed=SEED + 1):
    """Adds phase and carries fields to each flow."""
    r = generator(seed)
    for a in ak:
        a["phase"] = ("new", "established", "established", "new")[r(4)]
        a["carries"] = ("matching", "matching", "matching", "other")[r(4)]
    return ak


def policy(a):
    """Intent: only the DMZ's web entry from outside; nobody from outside to mgmt."""
    if a["source"] == "ext":
        return a["dest"] == "dmz" and a["port"] in (80, 443)
    if a["source"] == "dmz":
        return a["dest"] == "int" and a["port"] == 3306
    if a["source"] == "mgmt":
        return True
    return a["dest"] != "mgmt"


def diff(events, intent, device):
    d = {"correct_pass": 0, "correct_block": 0, "false_admit": 0, "false_deny": 0}
    for o in events:
        n, g = intent(o), device(o)
        if n and g:
            d["correct_pass"] += 1
        elif not n and not g:
            d["correct_block"] += 1
        elif g:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def rule_chain(rules):
    """First match wins; implicit deny at the end."""
    def run(a):
        for action, criteria in rules:
            if all(a[k] == v for k, v in criteria.items()):
                return action
        return False
    return run


NARROW = [(True, {"source": "ext", "dest": "dmz", "port": 80}),
          (True, {"source": "ext", "dest": "dmz", "port": 443}),
          (True, {"source": "dmz", "dest": "int", "port": 3306}),
          (True, {"source": "mgmt"}),
          (True, {"source": "int", "dest": "dmz"}),
          (True, {"source": "int", "dest": "ext"}),
          (True, {"source": "int", "dest": "int"})]
TRIPLE = ("source", "dest", "port")


def reverse(a):
    return {"source": a["dest"], "dest": a["source"], "port": a["port"]}


def mirror_rules(rules):
    """The mirror twin of every rule that writes both ends."""
    y = []
    for action, criteria in rules:
        if "source" in criteria and "dest" in criteria:
            t = dict(criteria)
            t["source"], t["dest"] = criteria["dest"], criteria["source"]
            y.append((action, t))
    return y


def visible(a, fields):
    return {k: v for k, v in a.items() if k in fields}


def intent(a):
    """The intent of an established flow is the intent of the side that opened it."""
    return policy(a if a["phase"] == "new" else reverse(a))


def packet_filter(rules):
    k = rule_chain(rules)
    return lambda a: k(visible(a, TRIPLE))


def stateful(rules):
    k = rule_chain(rules)
    return lambda a: k(visible(a, TRIPLE)) if a["phase"] == "new" else k(reverse(a))


def app_aware(rules):
    d = stateful(rules)
    return lambda a: d(a) and a["carries"] == "matching"


FLOWS = annotate(flows())
MIRRORED = NARROW + mirror_rules(NARROW)
TYPES = (("packet, unmirrored", NARROW, packet_filter(NARROW)),
         ("packet, mirrored", MIRRORED, packet_filter(MIRRORED)),
         ("stateful", NARROW, stateful(NARROW)),
         ("app aware", NARROW, app_aware(NARROW)))

print(f"flow {len(FLOWS)} | new {sum(a['phase'] == 'new' for a in FLOWS)} | "
      f"established {sum(a['phase'] == 'established' for a in FLOWS)} | "
      f"intent passes {sum(intent(a) for a in FLOWS)} | "
      f"off-entry {sum(a['carries'] == 'other' for a in FLOWS)}")
print()
print(f"{'mechanism':<22s} {'rules':>5s} {'correct':>7s} {'false admit':>12s} "
      f"{'false deny':>12s}")
for name, k, device in TYPES:
    s = diff(FLOWS, intent, device)
    print(f"{name:<22s} {len(k):5d} {s['correct_pass'] + s['correct_block']:7d} "
          f"{s['false_admit']:12d} {s['false_deny']:12d}")
print()
print("established flows the unmirrored filter blocks:",
      [a["no"] for a in FLOWS
       if a["phase"] == "established" and intent(a) and not packet_filter(NARROW)(a)])
print("false admits from both filters:",
      [(a["no"], a["source"], a["dest"], a["port"])
       for a in FLOWS if not intent(a) and packet_filter(MIRRORED)(a)])
print("false denies from the app-aware mechanism:",
      [(a["no"], a["source"], a["dest"], a["port"])
       for a in FLOWS if intent(a) and stateful(NARROW)(a)
       and not app_aware(NARROW)(a)])
```

```
flow 40 | new 20 | established 20 | intent passes 23 | off-entry 12

mechanism              rules correct  false admit   false deny
packet, unmirrored         7      27            6            7
packet, mirrored          13      30            6            4
stateful                   7      40            0            0
app aware                  7      32            0            8

established flows the unmirrored filter blocks: [12, 26, 27, 31, 32, 36, 37]
false admits from both filters: [(1, 'mgmt', 'int', 3306), (6, 'mgmt', 'int', 3306), (30, 'int', 'ext', 3306), (35, 'int', 'ext', 3306), (38, 'mgmt', 'int', 443), (40, 'int', 'ext', 443)]
false denies from the app-aware mechanism: [(7, 'mgmt', 'mgmt', 443), (8, 'mgmt', 'dmz', 443), (9, 'ext', 'dmz', 443), (15, 'mgmt', 'mgmt', 443), (20, 'int', 'dmz', 443), (21, 'int', 'dmz', 22), (32, 'ext', 'int', 443), (36, 'ext', 'int', 443)]
```

## What State Tracking Recovers

The unmirrored filter gives the same decision as intent on **27** of the forty flows. The
remaining thirteen split into **two separate errors**, and both must be written down.

**False admit: 6.** All six are established flows. In every one, the filter looks at the
packet's own direction and that direction matches a rule — but the direction that opened
the conversation did not. A packet returning from `mgmt` to `int` catches on the fourth
rule and passes; but `int` had opened the conversation, and going from `int` to `mgmt` was
the one thing intent forbade. The filter reads the packet correctly; it reads the
conversation wrong.

**False deny: 7.** All seven are established flows too. The return direction of a
conversation that was let through matches no rule, and the implicit deny cuts it off. No
record is produced that these seven flows were cut; there is no line in the chain that
stopped them.

When mirror rules are added, the chain grows from seven to **13** and false deny drops
from **7** to **4**. All three recovered flows are packets returning from `ext` to `int`;
what recovers them is the mirror twins of the third and sixth rules. The remaining four
cannot be recovered, because all of them return to the `mgmt` zone, and that zone's rule
writes only its source: **a rule that does not write both ends has no mirror.**

What the mirror does not pay for is the false-admit column: **from 6 to 6**, unchanged.
Because the mirror cannot tell a return packet apart from a new connection, it cannot fix
the error that was already there. Adding six rules grew the chain by 86%, brought the
deviation from intent down from thirteen to ten — and never touched one of the two
directions of that deviation.

The stateful mechanism gives **40/40** with the same seven rules: false admit **0**, false
deny **0**. The gain does not come from how the rule is written, but from the question
changing. For an established flow, the mechanism does not ask, "does this triple match a
rule"; it asks, "was this conversation let through when it was opened." The two questions
give different answers on thirteen of the forty flows.

## What Application Awareness Cannot Measure

The application-aware mechanism adds one condition to the stateful decision, and the
result **gets worse**: correct drops from **40** to **32**, and false deny climbs from
**0** to **8**. False admit stays at **0**.

This is not a flaw; it is the limit of the measure. Twelve of the forty flows carry an
application other than the one their port implies; the mechanism sees these and cuts them.
The oracle, however, never reads that field: when intent says "only to the DMZ's web entry
from outside," it describes **the entry by port**, not by the carried application. A
mechanism that takes as a criterion a field intent never used produces, against this
oracle, only false denies — no matter how right it is about that field.

The conclusion drawn from this is about intent, not about the mechanism. To be able to
measure what application awareness gains, **the policy itself must be rewritten in the
language of applications**. This is not a device feature but a policy decision; and until
it is written, the field the device sees in addition cannot enter the measurement.

## Surfaces Left Open, and Narrowings

**First surface — the mirror rule's direction.** The mirror rule between `ext` and `int`
carries no port; while letting a return packet through, it also lets through a **new**
external connection. The measurement produced zero false admits from this surface, because
there is no **new** flow from `ext` to `int` among the forty. Measuring zero does not show
the surface is closed; this is the resolution of the set. **Narrowing:** state tracking
instead of a mirror. The table opens the door only to conversations that were already
established, and it brings the false admit down from **6** to **0** on the same chain.

**Second surface — the directionless rule.** The fourth rule writes only its source: from
the management zone to every destination, every port. This breadth pays in both
directions — an unmeasurable opening in the forward direction, and **4** false denies in
the return direction because it has no mirror. **Narrowing:** writing the rule's
destination and port too. The cost of this is rule count, and measuring that cost is the
work of the next two lessons.

**Third surface — traffic that does not match its entry.** Twelve of the forty flows carry
something other than what their port implies. All three mechanisms that look at the port
let every one of these through, and the measurement does not show this as an error.
**Narrowing:** rewriting intent in the language of applications. The device reading more,
on its own, is not a narrowing; an intent that counts what is read as a criterion is
required.

## Summary

- The written boundary has two invariants: first match wins, and the chain ends in
  implicit deny. Because implicit deny has no rule name standing against it, it makes
  false deny invisible.
- What separates the three kinds is not how the rules are written but the input the rule
  is applied to: packet filtering reads the triple, the stateful mechanism reads the
  conversation's opening direction, the application-aware mechanism reads the carried
  application.
- With the same seven rules, the unmirrored filter gives **6** false admits and **7**
  false denies; the mirrored filter, with 13 rules, gives **6** and **4**; the stateful
  mechanism gives **0** and **0**.
- The mirror rule cuts false deny by roughly a third but never touches false admit, and it
  cannot imitate a rule that does not write both ends; it is no substitute for state
  tracking.
- The application-aware mechanism produces **8** false denies and does not reduce false
  admit; as long as intent is written in terms of port, the field it reads in addition
  cannot enter the measurement.

## Next Step

In this lesson the rule chain was held fixed and only the mechanism running it changed.
The chain itself was not examined: is seven the right number of rules, is their order the
right order, what does deleting a rule open up? The next lesson holds the mechanism fixed
and changes the chain. A short six-rule set, a long eight-rule set, and the seven-rule
narrow set are placed side by side on the same forty flows; what is measured is whether
there is any link between rule count and how well policy is met, and how a rule can end up
never being read because of a rule ahead of it.
