Skip to content
academia.sh

Lesson 08 / 14

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.

Contents

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:

# 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

"""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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close