---
title: 'Wireless Networks and Network Security'
source: 'https://academia.sh/en/courses/wireless-and-security'
language: en
updated: '2026-08-17T04:39:19+00:00'
license: 'CC BY-SA 4.0'
lessons: 14
outcomes:
    - 'Evaluates the coverage and interference constraints of a wireless network design'
    - 'Selects VPN technologies according to the threat model'
    - 'Applies network segmentation and zero trust principles'
---

# Wireless Networks and Network Security

In wireless the boundary is not a line but a curve, and in security it is an ordered sequence of rules; in both halves the course counts, in both directions, the gap between the boundary the design intended and what actually crosses it.

## Wireless Networks

The physics of transmission and interference, central management of access points, channel planning and cell coverage, client handovers, security generations, range-power trade-offs, and the main components of cellular architecture.

### Physics of Wireless Transmission

Source: https://academia.sh/en/courses/wireless-and-security/physics-of-wireless-transmission

In wireless, the link is not binary but a graded curve: in a forty-client setup, of the 27 clients inside a thirty-meter cell only 9 run at full rate tier, 14 drop to lower tiers, and 4 cannot associate at all.

The Switching and Routing course counted seventeen separate disagreements, and in every one of
them accepted a single thing without asking: if a cable connects two devices, the frame leaving
that cable reaches the other end. The link either exists or it does not. No third state besides
breakage was ever considered; if the tables disagree the packet goes to the wrong place, but on
its way to where it should go, it is never lost.

In wireless this acceptance falls away. The medium belongs to no one, its boundary is not
drawn, and transmission is not a certainty but a probability. A client's link is not "present"
or "absent"; it is a **curve** — it falls with distance, drops with interference, descends in
tiers, and reaches zero somewhere. This course's question is born right here: **what is the gap
between the boundary the design intends and what actually gets through?** This lesson builds the
wireless half of that gap and measures its most counterintuitive result: being inside the cell
is not the same as working.

#### The Shared Medium

In a wired link the transmission medium is split between two ends: whichever side plugs in the
cable uses it. In wireless the medium is **shared** and there is no record of who is using it.
Every transmitter broadcasting in the same frequency range is noise to every receiver listening
on that range — it makes no difference whether the transmitter belongs to the same network.

For this reason the usable frequency range is split into a **band**, and the band is divided
into **channels**. Two neighbors broadcasting on the same channel directly corrupt each other;
two broadcasts on adjacent channels can also bleed into each other from spillover. A channel
plan's job is to make sure that two access points close enough to hear each other do not land on
the same channel.

The table below is the **fictional** band layout this lesson uses. Real frequency allocations
vary from country to country, are updated by regulatory decisions, and affect nothing this
lesson measures; the numbers here are chosen only to show the shape of channel planning.

```text
# taught band layout , fictional and not executed

band N (narrow)  : 8 channels, channel width 1 unit
                   non-overlapping channel set: 1, 4, 7  (three channels)
                   range coefficient high, tier ceiling low

band W (wide)    : 24 channels, channel width 1 unit
                   non-overlapping channel set: all 24 channels
                   range coefficient low, tier ceiling high

bonding          : four channels in band W can bond into one 4-unit channel
                   -> 6 non-overlapping wide channels, tier ceiling doubled
```

The narrow band has only three non-overlapping channels; a fourth access point inevitably
reuses a neighbor's channel. The wide band has channels to spare, but range is short. This
trade-off will be measured in later lessons of the course; here only where interference comes
from is being established.

The effect of channel and band width on the transport layer was established in the Network
Models course; it is not repeated here. That a shared medium produces collisions is not new
either — the Switching and Routing course's first lesson counted this on the hub. **Where this
lesson departs is the measure itself:** there, what was measured was the number of colliding
frames; here, what is measured is in how many clients, and in **which two directions**, the
design's promise diverges.

#### Signal Strength

What the receiver sees is called **signal strength**, and two things bring it down: distance
and interference. This lesson combines the two on a single fictional scale.

$$
\text{signal} = \text{power} - \text{distance} - \text{interference}
$$

Here `power` is the access point's signal budget, `distance` is in meters, and `interference`
is the contribution from neighbors using the same channel at that client's location. All three
are fictional units.

In real propagation, attenuation is not a linear function of distance; walls, reflections, and
bodies each take their own share. The linear form here is a **fiction** chosen to keep the
measurement readable, and all of this lesson's claims live inside that fiction. The one thing
the fiction carries is this: signal strength is a continuous quantity, and it drops for two
independent reasons.

#### Rate Tiers

Even though signal strength is continuous, transmission is not. Based on the signal strength it
measures, the transmitter picks a **rate tier**: a dense modulation when the signal is high, a
more resilient but slower modulation as it drops. Tier transitions are threshold-bound.

| Signal strength | Rate tier | Meaning |
|---|---|---|
| 34 and above | 6 | full rate |
| 28 – 33 | 4 | one tier down |
| 22 – 27 | 2 | two tiers down |
| 18 – 21 | 1 | lowest tier |
| below 18 | 0 | cannot associate |

The last row is a separate threshold. When signal strength drops below 18, even the most
resilient modulation does not hold, and the client cannot **associate**. Association is the
establishment of a link between the client and the access point; until it is established, there
is no network for the client.

The existence of tiers is what separates wireless from a wired link. A wired link either works
or breaks; a wireless link can run **six times slower**, and no connection status indicator
reports this. Indicators show association, not tier.

#### The Asymmetric Bond and Shared Time

The calculation above was made for a single direction. In reality every link has two
directions, and the two are not equal. The access point is a mains-powered device with a high
signal budget; the client runs on a battery and its budget is low. The result is an
**asymmetric link**: the client may hear the access point while the access point does not hear
the client. On the user's side the network is visible, sits in the list, can be selected — and
association never forms. Because this lesson measures a single direction, it does not put a
number on the asymmetry; but this is the source of the "I can see the network but cannot
connect" symptom, and it does not look like a coverage problem.

The second difference is that the medium is **half duplex**. While a transmitter is
broadcasting, its own transmission fills its own receiver; it cannot hear anyone else
broadcasting at the same time. In a wired link, collision is **sensed** — the sender listens to
the signal and sees the corruption. In wireless it cannot be sensed, so the mechanism is built
on **avoiding** collision rather than detecting it: the transmitter listens to the medium, and
if it finds it idle, waits a further random interval before starting to transmit. There is no
referee for who uses the medium; order is born from the waiting itself.

When these two facts combine, the measured effect of a tier drop multiplies. A client at tier 1
holds the medium **six times** as long as a client at tier 6 to carry the same data; this
follows directly from the definition of the tier numbers. Because the medium is shared, everyone
in the cell waits for that entire span. A single slow client at the cell's edge slows down every
fast client at the center. In the measurement, clients that drop to lower tiers do not just lose
their own share; they diminish everyone else's too.

#### The Design's Promise

The promise made when a wireless network is designed is a radius: "the client inside the
thirty-meter cell runs at full rate." This promise is the course's **intent**, and it is the
measurement's **oracle**. Because we write the fiction ourselves, we know every client's real
distance; what the intent means is beyond dispute.

The medium's answer, on the other hand, is a separate function: can the client associate?
Comparing the two produces four outcomes, and the whole course counts this foursome.

- **Correct admit** — intent would admit, the medium admitted.
- **Correct deny** — intent would deny, the medium denied.
- **False admit** — intent would deny, the medium admitted. A client outside the cell
  associated.
- **False deny** — intent would admit, the medium denied. A client inside the cell could not
  associate.

**The two directions are counted separately.** A measurement that counts only false admit
cannot see where coverage falls short; one that counts only false deny cannot see leaking
coverage. Throughout the course both stand side by side in every table.

The measurement's assumptions:

- **WN1.** Forty clients' distance and the interference they see come from a single generator.
  Distance is in the 5–45 m range, interference in 0–12 units; the oracle is known because we
  produced the fiction ourselves.
- **WN2.** Signal strength is linear, in the form power − distance − interference. This is not
  a propagation model; it is a fiction that keeps the tiers readable.
- **WN3.** There is a single access point, and its signal budget is 52. This lesson does not
  set up another power value; a power sweep is the subject of the channel planning lesson.
- **WN4.** The cell radius is 30 m, and intent looks only at distance. Intent does not see
  interference — the design does not know what neighbors will do when it makes its promise.
- **WN5.** The tier thresholds are 34, 28, 22, and 18; 18 is also the association threshold.
  The medium's decision is read as `rate_tier > 0`, which is association itself.
- **WN6.** The set is forty clients; the smallest measurable difference in this set is
  **1/40 = 0.025**. A difference smaller than this is not defended by this measurement.

#### Measurement

```python
"""Physics of wireless transmission: signal strength, rate tier, and the cell's promise."""
SEED = 20260811
POWER = 52           # access point's signal budget, fictional unit
RADIUS = 30          # cell radius the design promises, meters
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))   # (signal threshold, rate tier)


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

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


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def signal(c, power=POWER):
    return power - c["distance"] - c["interference"]


def rate_tier(c, power=POWER):
    for threshold, tier in TIERS:
        if signal(c, power) >= threshold:
            return tier
    return 0


def cell_intent(c, radius=RADIUS):
    """The design's promise: the client inside the cell runs at full rate."""
    return c["distance"] <= radius


def medium(c, power=POWER):
    return rate_tier(c, power) > 0


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


pool = clients()
print(f"clients {len(pool)} | distance {min(c['distance'] for c in pool)}-"
      f"{max(c['distance'] for c in pool)} m | interference "
      f"{min(c['interference'] for c in pool)}-{max(c['interference'] for c in pool)} | "
      f"inside cell {sum(1 for c in pool if cell_intent(c))}")
print()
print(f"{'tier':>6s} {'clients':>8s} {'lowest signal':>14s}")
for threshold, tier in TIERS:
    k = [c for c in pool if rate_tier(c) == tier]
    print(f"{tier:6d} {len(k):8d} {min((signal(c) for c in k), default=0):14d}")
zero = [c for c in pool if rate_tier(c) == 0]
print(f"{0:6d} {len(zero):8d} {max(signal(c) for c in zero):14d}")
print()

d = gap(pool, cell_intent, medium)
print(f"{'correct admit':>13s} {'correct deny':>12s} {'false admit':>12s} "
      f"{'false deny':>12s}")
print(f"{d['correct_admit']:13d} {d['correct_deny']:12d} {d['false_admit']:12d} "
      f"{d['false_deny']:12d}")
print()

inside = [c for c in pool if cell_intent(c)]
print(f"{'tier inside cell':>17s} {'clients':>8s}")
for tier in (6, 4, 2, 1, 0):
    print(f"{tier:17d} {sum(1 for c in inside if rate_tier(c) == tier):8d}")
print()
print("false deny (no, distance, interference, signal):",
      [(c["no"], c["distance"], c["interference"], signal(c)) for c in inside if rate_tier(c) == 0])
```

```
clients 40 | distance 6-44 m | interference 0-12 | inside cell 27

  tier  clients  lowest signal
     6        9             35
     4        4             30
     2        8             22
     1        2             18
     0       17             17

correct admit correct deny  false admit   false deny
           23           13            0            4

 tier inside cell  clients
                6        9
                4        4
                2        8
                1        2
                0        4

false deny (no, distance, interference, signal): [(4, 24, 11, 17), (12, 30, 6, 16), (25, 30, 12, 10), (32, 30, 7, 15)]
```

#### Associating Is Not the Same as Working

The second table distributes forty clients across tiers. **9** clients are at full rate, **4**
are one tier down, **8** are two tiers down, **2** are at the lowest tier, and **17** cannot
associate at all. The total associated is 23.

The third table compares intent with the medium. There are **27** clients inside the cell;
**23** of them associate, **4** cannot. None of the 13 clients outside the cell associate:
**false admit 0**. At this power the boundary does not leak. An audit that looked only at this
column would declare the configuration flawless.

The fourth table gives what that audit cannot see. The tier distribution of the 27 clients
inside the cell is **9, 4, 8, 2, 4**. The design's promise was "the client inside the cell runs
at **full rate**." The number of clients for whom that promise is truly kept is **9** — a third
of those inside the cell. **14** clients associate, but at lower tiers, and **4** cannot
associate at all.

The reading that follows is the course's fourth reading: **the boundary is not binary.** The
distinction between "admitted" and "not admitted" is a coarse reading of the tier sequence. A
configuration can show zero error on this coarse reading while failing to keep two-thirds of the
promise it made.

Where the four false-deny clients sit is instructive too. All four are inside the cell but
close to its edge: their distances are between 24 and 30 m, and the interference they see is
high. Distance alone is not enough — **intent never sees interference at all** (WN4). When the
design draws the cell, it does not know which channel the neighboring access points will
broadcast on; interference is a quantity that appears after the promise has already been made.

This shows why counting the two directions separately is necessary. Leakage is zero, the
shortfall is four. A report that wants a single number will call this configuration either
flawless or flawed; either way it will be wrong.

Four clients' share of a forty-client set is **0.100**. Since the smallest measurable
difference in this set is 1/40 = 0.025 (WN6), four sits comfortably inside the measurement band
and cannot be called noise. A one-client difference, by contrast, is defensible with this set
but weak; this bound will be recalled throughout the course whenever single-client shifts are
interpreted.

#### Summary

- In wireless the medium is shared and transmission is not a certainty; the link is not a
  binary state but a curve that falls with distance and drops with interference.
- Signal strength strikes tier thresholds at 34, 28, 22, and 18. The last threshold is also
  the association threshold; there is no network for a client that drops below it.
- In the forty-client fiction at power 52 the tier distribution is **9, 4, 8, 2** and **17**
  unable to associate; the total associated is 23.
- In the same configuration **false admit is 0** and **false deny is 4**: the boundary does not
  leak, but it leaves four clients inside the cell out.
- Only **9** of the 27 clients inside the cell are at the full rate tier. Associating is not
  the same as working, and a one-directional audit cannot see it.

#### Next Step

This measurement counts the decision a single access point makes on its own: it associates the
client if it hears it, and does not if it does not. In a building, though, there are dozens of
access points, and each one's signal budget, channel, and threshold is a separately given
value. The next lesson measures where that decision is made: does each access point decide by
its own view, or does a shared controller decide? If the same client can get two different
answers from two access points, the owner of the answer is not the client's condition but which
one it happens to land on.

### Access Points and Controllers

Source: https://academia.sh/en/courses/wireless-and-security/access-points-and-controllers

Whether the decision is made at the access point or at the controller is measured: three points running on separately given budgets associate 25 clients and produce 2 false admits, a shared policy associates 23 clients and leaks zero, and the clients running at full rate rise from 5 to 9.

The previous lesson counted the decision a single access point makes on its own: it
associates the client if it hears it above the threshold, and does not if it does not. 23 of
the forty clients associated, 4 inside the cell could not, and no one leaked in from outside.

In a building, though, there is no single access point. There are dozens of points, and each
one's signal budget, channel, and threshold is a given value — set somewhere, at some time, by
someone. This lesson's question is: **who gives that value, and where is the decision made?**
If the same client can get two different answers from two access points, the owner of the
answer is not the client's condition but which one it happens to land on.

#### Association Is an Exchange

A client "seeing" the network and connecting to it are separate matters. A frame exchange
stands between the two, and the decision is made at a specific step of that exchange.

```text
# taught management frame sequence , fictional and not executed

1. access point -> everyone     beacon
                                  network: measurement.test
                                  channel: 4 , band: N
                                  supported tiers: 6 4 2 1

2. client -> everyone           probe request
                                  network: measurement.test

3. access point -> client       probe response
                                  measured signal strength: 26

4. client -> access point       association request
                                  requested tier: 4

5. access point -> client       association response
                                  result: accept , granted tier: 2
                                  <-- THE DECISION IS MADE AT THIS STEP
```

The fifth step is where the decision is made, and it says two things at once: was the client
accepted, and at which rate tier. A "no" answer to the first question means, in the client's
eyes, that the network does not exist; the answer to the second question shows up in no
indicator at all.

The critical point is this: **who writes** the fifth step? The access point sends the frame,
but the rule that determines the answer's content may be defined somewhere else entirely.

#### Two Management Models

An **independent access point** carries its own configuration. Its channel, signal budget, and
threshold live in its own memory; it makes the association decision by looking only at the
signal strength it itself measures. It does not know which channel its neighbor is on, what
budget it broadcasts with, or which client it rejected.

The **controller-based model** splits the decision in two. The **control plane** is
centralized: channel assignments, signal budgets, thresholds, and policies are held in a single
place and distributed to the points. The **data plane** can stay local, or it can be tunnelled
to the controller; that is a separate choice, and not what this lesson measures.

What the controller really carries is a **shared view**. When every point's measurements are
collected in a single place, things a point could never know on its own become knowable: where
a channel is being reused, how many points hear a given client, where one point's budget cuts
into its neighbor's coverage.

This resembles the Switching and Routing course's axis, but it is not the same. There, the
question asked was whether copies of a shared truth agreed with each other; here, the question
asked is whether copies **exist at all**. Independent points do not carry stale copies of a
shared truth — there is no shared truth; each point carries its own value alone.

The controller does not have to be a device, either. It can be dedicated hardware, a server on
the network, or a role taken on by one of the points; nothing changes from the measurement's
point of view. What is measured is not the box itself but **whether the decision comes from a
single place**.

#### Where the Data Plane Flows

Centralizing the control plane does not centralize the data plane; that is a second, separate
choice.

With **local switching**, the client's frame leaves the access point and enters the local
network directly. The controller only supplies the rule; it never sees the traffic. The path is
short, traffic in flight is unaffected if the controller is cut off, but enforcing the policy
depends on the points being correctly configured.

With a **tunnelled data plane**, the frame is carried to the controller first, and exits to the
network from there. What this buys is a policy enforced from a single point: all wireless
traffic passes through the same place and hits the same rule. What it costs is the path — even
two clients standing side by side have their frames travel all the way to the controller and
back — and the controller becoming a bottleneck.

The choice is not binary either: some traffic can be left local while other traffic is
tunnelled. This lesson's measurement changes only the control plane; where the data plane flows
does not enter the association decision and does not mix into the measurement.

#### Configuration Drift

The independent model's countable flaw is **configuration drift**. Points are not set up at
the same time. The first point is raised in response to a coverage complaint, the second stays
as installed, the third is cut back in response to an interference complaint. Every step is
reasonable on its own; none of them leaves a record, and no one sees the total.

The inevitability of drift is a scale problem. The interaction of two points can be held in
mind; the forty-five pairwise interactions of ten points cannot. Raising one point's budget does
not only grow that point's coverage; it also changes where its neighbors' coverage gets cut
off. Deciding point by point means there is no place at all that can see this interaction.

The result is three different boundaries drawn within the same network. The measurement sets
this up with three budgets: one point at 58, one at 52, one at 46. Set against it, the
controller regime distributes a single value, 52, to all three points. The only thing that
changes is **where the decision is made**; the clients, the distances, the interference, and
the intent all stay the same.

The measurement's assumptions:

- **WN7.** Forty clients come from the previous lesson's fiction; distance and interference
  are the same. The oracle is again the cell intent: the client inside the 30 m radius cell
  should have been admitted.
- **WN8.** Which access point hears which client comes from a separate generator and is
  independent of distance. The `distance` field is the distance to the point that hears the
  client.
- **WN9.** There are three points, and their budgets in the independent regime are 58, 52, and
  46. These three values are the fiction of configuration drift, not a power sweep; the sweep
  itself is the subject of the next lesson.
- **WN10.** In the controller regime, all three points' budget is 52. This is the only thing
  that changes between the two regimes.
- **WN11.** The controller's distribution is instant and lossless. Distribution delay and the
  controller becoming unreachable are not measured; both are addressed separately in the text.
- **WN12.** The consistency measurement evaluates the same client separately against three
  budgets. This is not a movement fiction; the client stays put, only the evaluating point
  changes. The client actually changing location is the subject of the roaming lesson.
- **WN13.** The set is forty clients; the smallest measurable difference is **1/40 = 0.025**.

#### Measurement

```python
"""Where the decision sits: independent access points, or a shared controller."""
SEED = 20260811
PLACEMENT = 20260812     # which access point hears which client
RADIUS = 30
SHARED_POWER = 52        # the single value the controller distributes to every point
POINTS = (("A", 58), ("B", 52), ("C", 46))    # separately given budgets
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))


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

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


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    y = generator(PLACEMENT)
    for c in pool:
        c["point"] = y(3)
    return pool


def signal(c, power):
    return power - c["distance"] - c["interference"]


def rate_tier(c, power):
    for threshold, tier in TIERS:
        if signal(c, power) >= threshold:
            return tier
    return 0


def cell_intent(c, radius=RADIUS):
    return c["distance"] <= radius


def medium(c, power):
    return rate_tier(c, power) > 0


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def local(c):
    """Decision at the access point: the budget of the point that hears the client applies."""
    return medium(c, POINTS[c["point"]][1])


def central(c):
    """Decision at the controller: one policy across every point."""
    return medium(c, SHARED_POWER)


pool = clients()
print(f"{'point':>5s} {'budget':>6s} {'clients':>8s} {'associated':>10s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for i, (name, power) in enumerate(POINTS):
    k = [c for c in pool if c["point"] == i]
    d = gap(k, cell_intent, lambda c, p=power: medium(c, p))
    print(f"{name:>5s} {power:6d} {len(k):8d} {d['correct_admit'] + d['false_admit']:10d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")
print()

print(f"{'regime':<12s} {'associated':>10s} {'full rate':>9s} {'correct':>7s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for name, mechanism, tier_fn in (("independent", local, lambda c: rate_tier(c, POINTS[c["point"]][1])),
                                  ("controller", central, lambda c: rate_tier(c, SHARED_POWER))):
    d = gap(pool, cell_intent, mechanism)
    print(f"{name:<12s} {d['correct_admit'] + d['false_admit']:10d} "
          f"{sum(1 for c in pool if tier_fn(c) == 6):9d} "
          f"{d['correct_admit'] + d['correct_deny']:7d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")
print()

diverging = [c for c in pool if len({medium(c, p) for _, p in POINTS}) > 1]
tier_diverging = [c for c in pool if len({rate_tier(c, p) for _, p in POINTS}) > 1]
print(f"clients whose association decision changes point to point: {len(diverging)}")
print(f"clients whose rate tier changes point to point: {len(tier_diverging)}")
print()

print(f"{'point pair':<11s} {'only first':>11s} {'only second':>12s} "
      f"{'tier differs':>13s}")
for label, g1, g2 in (("A / B", 58, 52), ("B / C", 52, 46), ("A / C", 58, 46)):
    print(f"{label:<11s} {sum(1 for c in pool if medium(c, g1) and not medium(c, g2)):11d} "
          f"{sum(1 for c in pool if medium(c, g2) and not medium(c, g1)):12d} "
          f"{sum(1 for c in pool if rate_tier(c, g2) != rate_tier(c, g1)):13d}")
```

```
point budget  clients associated  false admit   false deny
    A     58       10          8            2            0
    B     52       18          9            0            2
    C     46       12          8            0            2

regime       associated full rate correct  false admit   false deny
independent          25         5      34            2            4
controller           23         9      36            0            4

clients whose association decision changes point to point: 14
clients whose rate tier changes point to point: 29

point pair   only first  only second  tier differs
A / B                 8            0            22
B / C                 6            0            21
A / C                14            0            29
```

#### More Connect, Fewer Work

The independent regime associates **25** clients, the controller regime **23**. A comparison
that looked only at this column would declare the independent model the winner: two more
clients connected.

The column next to it upsets this. Clients running at full rate tier are **5** in the
independent regime, **9** in the controller regime. More clients associate and fewer work. The
reason shows up in the table above: point A, pulled up to a budget of 58, gathers its share of
clients from too far away, while point C, cut down to 46, drops its share into lower tiers. The
average is a distribution that loses at both ends.

Reading the two directions separately clarifies the table further. **False admit** is 2 in the
independent regime, 0 in the controller regime. Both leaks come from point A — the table above
shows this directly: A's false admit is 2, the other two's is 0. **False deny**, on the other
hand, is **4 in both regimes**.

This equality is the lesson's most important line. Raising one point's budget **bought two
leaks and did not reduce the shortfall by a single client.** The coverage gain went not to
where it was needed but outside the area already covered. This is the course's second claim:
**growing the apparatus does not bring it closer to intent.**

#### Who Owns the Decision

The bottom two lines measure consistency. For **14** of the forty clients the association
decision changes depending on which point evaluates it; for **29** the rate tier changes. So
for roughly two-thirds of the forty clients, the answer to "what does this client get" is
written not in the client's condition but in which one it lands on.

The last table separates the pairs. Between A and C, **14** clients can associate only with A,
and **none** can associate only with C — the asymmetry is total, because a lower budget only
costs. The number of clients whose tier differs is **22** between A and B, **21** between B and
C, **29** between A and C.

These numbers do not look like a coverage problem. A complaining user says "the network is slow
in that corner of the building"; a measurement finds no coverage gap in that corner, because
there is none. What has to be found instead is that the point covering that corner runs on a
different budget.

A direct consequence of the inconsistency is paid in roaming. As a client moves between two
points, it decides "I'm staying here" versus "I'm moving to the other one" based on the signal
strength it itself measures; and the signal it measures depends on the point's budget. A
high-budget point keeps holding a client from far away, and the handover comes late; a
low-budget point lets the client go early. When two budgets stand side by side within the same
network, the handover threshold does not mean the same thing everywhere in the network. What
this lesson measures is only the **separation** of the decision; when the handover itself
should happen, and early versus late handover being two separate errors, is the subject of the
roaming lesson.

#### The Controller's Own Flaws

The controller model's measured advantage is not free, and it is paid in two places.

The first is a **single point of failure**. What do the points do when the controller becomes
unreachable? There are two behaviors: either they stop new association and preserve the
policy, or they keep running with the last configuration in hand, **falling back to
standalone**. If the second is chosen and the outage runs long, the network slowly drifts back
to the independent regime — this time with no one noticing. The measurement's independent row
is the state at the end of a long stretch spent without a controller.

The second is a **shared flaw**. In the independent model, a wrong value breaks one point; in
the controller model, a wrong value is distributed to **every** point. The controller regime in
the measurement leaks zero because it distributes 52; had it distributed 58, the leak would have
spread across the whole network. A shared view shares the shared error too.

Both of these flaws are narrowed by **recording** the configuration: if what value was
distributed, when it changed, and which point it reached are written down, drift becomes a
matter of measurement. If it is not written down, drift shows up only through user complaints,
and the complaint points to the wrong place.

#### Summary

- Association is a frame exchange, and the decision is made at the last step; that step writes
  both the acceptance and the granted rate tier, and the second shows up in no indicator.
- An independent access point decides with its own budget and does not know its neighbor; the
  controller model builds a shared view by centralizing the control plane.
- Configuration drift is a measurable flaw: three separate budgets associate **25** clients and
  produce **2** false admits, a shared policy associates **23** and leaks **0**.
- **False deny is 4 in both regimes.** Raising one point's budget bought only leakage; it never
  reduced the shortfall.
- For **14** of the forty clients the association decision, and for **29** the rate tier,
  changes with which point they land on; this does not look like a coverage gap.

#### Next Step

In this measurement the budgets came from the fiction, and why they were set to those values
was never asked. Yet a wireless network's design is exactly this question: which channel, which
budget goes to each point, and where should the cells touch? The next lesson sweeps the budget
alone — 58, 52, 46, and 40 — and counts both directions at every value. There it will be seen
that lowering the budget zeroes out the leak, and the moment it does, the shortfall it leaves
inside the cell grows silently.

### Channel Planning and Coverage

Source: https://academia.sh/en/courses/wireless-and-security/channel-planning-and-coverage

At signal budget 58, 52, 46, and 40 the associated clients are 31, 23, 17, and 13; false admit is 5, 0, 0, 0 and false deny is 1, 4, 10, 14 — the configuration that zeroes out the leak leaves ten clients inside the cell out.

The previous lesson took the access points' budgets from the fiction and never asked why those
values were what they were. The measurement showed that three separate budgets gave the same
client three separate answers: for 14 of the forty clients the association decision, and for 29
the rate tier, changed depending on which point it happened to land on.

So what should that budget have been? A wireless network's design is exactly this question, and
it has two parts: which channel, which signal budget goes to each point. What this lesson
measures is what changing the budget alone does to both directions at once. The result is the
course's most counterintuitive table: the configuration that zeroes out the leak is the
configuration that breaks the design's promise the most.

#### Cell, Overlap, and Reuse

The area an access point covers is called a **cell**. When cells tile an area, two constraints
operate at once, and they pull in opposite directions.

The first constraint is **overlap**. If cells never overlap, gaps remain between them where the
client hears no point at all; the client loses its bond while walking and has to re-establish it.
For this reason neighboring cells' edges are deliberately overlapped.

The second constraint is **reuse distance**. Two overlapping cells on the same channel are
interference to each other. Because the number of channels is limited, the same channel is
reused sooner or later; the plan tries to make sure that repetition happens far enough away.

```text
# taught channel reuse pattern , fictional and not executed

band N, non-overlapping channel set: 1, 4, 7   (three channels)

  top floor      [1] [4] [7] [1]
  middle floor   [7] [1] [4] [7]
  bottom floor   [4] [7] [1] [4]

  the same channel sits three cells away from its nearest neighbor
  horizontal overlap : ~20% of the cell radius
  vertical leakage   : if inter-floor attenuation is low, the floor above also produces interference

band W, non-overlapping channel set: 24 channels
  the same channel never repeats within the pattern -> interference from within the plan ~0
  cost: range is short, the same area needs more points
```

The pattern is three-dimensional, and this is often overlooked. Two well-separated channels on
the same floor can be identical to the channel used by the neighbor on the floor above; if
inter-floor attenuation is low, vertical interference can exceed horizontal interference. The
numbers above are **fictional**; real bands' channel counts and which ranges are usable vary
from country to country and are updated by regulatory decisions. Nothing this lesson measures
depends on those numbers.

#### What a Site Survey Measures

The plan is made at a desk; its verification happens on site. A **site survey** walks point to
point through the building and records, at every location, which point is heard at which signal
strength. Its output is a coverage map.

What the map says and what it does not say must be kept separate. The map gives the association
boundary well: where no point is heard above the threshold. It gives the rate tier only for the
instant it measured; interference changes through the day, neighboring networks turn on and off,
human bodies cut the signal path. And the map does not know intent at all — where the design
promised full rate is not written on the map, it is written in the plan.

This lesson does not run a site survey. We know the fictional forty clients' real distance and
real interference; that is the oracle. What is measured is, for how many clients the given
budget departs from this known truth.

For a survey to make the two directions countable, it has to record three things at once: where
the measurement point is, the signal strength measured at that point, and which cell the plan
counts that point inside. If the third is not written down, false admit cannot be separated from
false deny — all that is left is a power map, and the map cannot say which measurement is
outside intent. When the measurement record and the plan record live in separate files, this
separation is in practice most often not made.

The measurement's assumptions:

- **WN14.** Forty clients; distance and interference come from the first lesson's fiction. There
  are 27 clients inside the cell, 13 outside.
- **WN15.** Intent does not change: the client inside the 30 m radius cell should have been
  admitted. The budget sweep changes only the mechanism, not the intent.
- **WN16.** The swept budgets are 58, 52, 46, and 40. The 6-unit step between them is fictional
  and is on the same order of magnitude as the spacing between tier thresholds.
- **WN17.** A single access point is measured. The contribution from neighboring points is
  folded into the interference field; it is not modeled as a separate point.
- **WN18.** The channel plan is modeled as a mechanism that puts a **ceiling** on interference.
  The better the plan, the lower the ceiling; ceiling 12 corresponds to the unplanned state,
  ceiling 0 to no interference at all.
- **WN19.** In the interference-ceiling measurement the budget is held fixed at 52. **One thing
  changes at a time:** either the budget or the ceiling.
- **WN20.** The full rate column is the number of clients at tier 6; the associated column is
  the number whose tier is greater than zero.
- **WN21.** The set is forty clients; the smallest measurable difference is **1/40 = 0.025**.

#### Measurement

```python
"""Cell design: budget sweep and the channel plan's interference ceiling."""
SEED = 20260811
RADIUS = 30
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))


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

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


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def signal(c, power=52, ceiling=12):
    """The channel plan puts a ceiling on interference; the better the plan, the lower the ceiling."""
    return power - c["distance"] - min(c["interference"], ceiling)


def rate_tier(c, power=52, ceiling=12):
    for threshold, tier in TIERS:
        if signal(c, power, ceiling) >= threshold:
            return tier
    return 0


def cell_intent(c, radius=RADIUS):
    return c["distance"] <= radius


def medium(c, power=52, ceiling=12):
    return rate_tier(c, power, ceiling) > 0


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


pool = clients()
print(f"clients {len(pool)} | inside cell {sum(1 for c in pool if cell_intent(c))} "
      f"| outside cell {sum(1 for c in pool if not cell_intent(c))}")
print()

print(f"{'power':>5s} {'associated':>10s} {'full rate':>9s} {'false admit':>12s} "
      f"{'false deny':>12s} {'total deviation':>15s}")
for power in (58, 52, 46, 40):
    d = gap(pool, cell_intent, lambda c, p=power: medium(c, p))
    print(f"{power:5d} {d['correct_admit'] + d['false_admit']:10d} "
          f"{sum(1 for c in pool if rate_tier(c, power) == 6):9d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d} {d['false_admit'] + d['false_deny']:15d}")
print()

print(f"{'interference ceiling':>21s} {'associated':>10s} {'full rate':>9s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for ceiling in (12, 8, 4, 0):
    d = gap(pool, cell_intent, lambda c, t=ceiling: medium(c, 52, t))
    print(f"{ceiling:21d} {d['correct_admit'] + d['false_admit']:10d} "
          f"{sum(1 for c in pool if rate_tier(c, 52, ceiling) == 6):9d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")
print()

print("clients left outside at power 46 despite being inside the cell (no, distance, interference):")
print([(c["no"], c["distance"], c["interference"]) for c in pool
       if cell_intent(c) and not medium(c, 46)])
print()
print("clients outside the cell leaking in at power 58 (no, distance, interference):")
print([(c["no"], c["distance"], c["interference"]) for c in pool
       if not cell_intent(c) and medium(c, 58)])
```

```
clients 40 | inside cell 27 | outside cell 13

power associated full rate  false admit   false deny total deviation
   58         31        13            5            1               6
   52         23         9            0            4               4
   46         17         2            0           10              10
   40         13         0            0           14              14

 interference ceiling associated full rate  false admit   false deny
                   12         23         9            0            4
                    8         24         9            0            3
                    4         27         9            0            0
                    0         32        13            5            0

clients left outside at power 46 despite being inside the cell (no, distance, interference):
[(4, 24, 11), (8, 20, 9), (10, 29, 0), (12, 30, 6), (13, 29, 2), (20, 25, 5), (25, 30, 12), (27, 20, 10), (32, 30, 7), (34, 23, 11)]

clients outside the cell leaking in at power 58 (no, distance, interference):
[(6, 37, 3), (14, 32, 7), (16, 33, 5), (28, 33, 2), (31, 35, 0)]
```

#### Growing Does Not Bring You Closer to Intent

The top table lowers the budget from 58 to 40 and shows four numbers at once.

At budget 58 the associated clients are 31 — eight more than the 23 at 52. The coverage gain is
real. But 5 of the eight gained are outside the cell: the last table shows their distances
between 32 and 37 m, all in territory the design never promised. The shortfall inside the cell
drops only from 4 to 1. So **three** of the eight-client gain went to intent and **five**
leaked outside.

**The rule is: growing the apparatus does not bring it closer to intent.** Raising the budget
grows the cell's boundary in every direction at once, and it cannot choose where that growth
goes. The coverage gained is paid for with coverage that leaks.

The total deviation column collects this into a single number: **6, 4, 10, 14**. The lowest
value is at 52. Neither the highest budget nor the lowest is closest to intent; the one closest
to intent is the budget that matches the radius the promise was made for. A budget is not right
or wrong as "more" or "less" — it is right or wrong **relative to the radius**.

#### What Zero Leakage Hides

The bottom three rows pay the course's third claim. At budget 46 false admit is 0, at 40 it is
again 0. An audit that reports only this column finds both configurations flawless — it even
finds 52 flawless, because there is no leak there either. Three configurations look identical.

The other column separates the three. False deny is 4 at 52, 10 at 46, 14 at 40. At power 46,
ten clients inside the cell cannot associate at all; at 40, fourteen. Given that there are 27
clients inside the cell, at budget 40 more than half of those inside are left out. The design's
promise is not kept, and the leakage column never shows it.

The list of the ten clients left outside says a second thing. Their distances range from 20 to
30 m — some of them stand right at the cell's center. Clients 8 and 27 sit at 20 m and still
cannot associate; both have high interference. **Cutting the budget hits the client with
interference first**, because the two are added together in the same calculation.

The practical rule that follows: a wireless network's report cannot be given as a single number.
"No leakage" and "full coverage" are separate claims, and one does not support the other.

#### For Coverage, or For Capacity

Reading the 46 and 40 rows in the table as "the wrong budget" would be incomplete. What makes
them fall short is not the budget itself but that the fiction carries a **single** access point
(WN17). Tiling the same area with more, smaller cells turns the low budget into the right
budget.

Two design goals separate here. **Design for coverage** tries to leave no gaps with the fewest
possible points: cells are large, budgets are high, the same channel is reused more often.
**Design for capacity** does the opposite: cells are shrunk, budgets are cut, the number of
points is increased. A small cell carries fewer clients and the queue waiting for the medium
gets shorter; also, because every client is closer to its point, its **tier rises**.

The measurement's fourth column shows why this second goal exists. At budget 52, only 9 of the
27 clients inside the cell are at full rate. Raising the budget does not fix this — at 58, full
rate is 13, but 5 leaks come with it. The way to actually raise full rate is to bring the client
closer to the point, that is, to build more cells.

**Channel width** is the second face of the same trade-off. Bonding four channels into one wide
channel raises the top speed a client can get, but it divides the number of non-overlapping
channels by four and shortens the reuse distance. In a dense area, a wide channel takes back the
speed it gained as interference; in a sparse area, the gain holds. Channel width is therefore
not a performance setting but a **plan decision**.

#### What the Channel Plan Contributes

The bottom table fixes the budget at 52 and changes only the interference ceiling — that is, it
improves only the channel plan.

In the unplanned state (ceiling 12) associated is 23, false deny is 4. As the ceiling drops to 8
the shortfall drops to 3, and to 4 it drops to 0: associated becomes 27, exactly the number of
clients inside the cell. **False admit is also 0.** The channel plan met intent exactly, without
touching the budget at all.

The last row is the unexpected one. When interference is removed entirely (ceiling 0), associated
jumps to 32 and false admit shoots from 0 to 5. The cell grows larger than the design drew it.
The reason is clear: what draws the boundary is not the budget alone but **budget minus
interference**. The radius that matched intent had been tuned assuming a certain interference
level; once interference is gone, the same budget reaches farther.

This changes how channel planning has to be read. Reducing interference is not, on its own, an
improvement; it is a **second dial** that has to be tuned together with the budget. Improving
the plan and leaving the budget as it is can turn a configuration that does not leak into one
that does.

The full rate column, meanwhile, holds steady across every row. Even at ceiling 4, where intent
is met exactly, the number of clients running at full rate is 9 — a third of the 27 inside the
cell. Association being flawless in both directions does not mean working is flawless.

The measurability of the differences in this table also has to be checked. False deny dropping
from 4 to 0 is a **0.100** share of the forty-client set, four times the smallest measurable
difference (WN21, **0.025**). False admit rising from 0 to 5 is **0.125**. Both sit comfortably
inside the band. But the shortfall dropping from 4 to 3 as the ceiling drops from 12 to 8 is a
single client and sits right at the boundary; a trend can be read from that step, not a
conclusion.

#### Summary

- Cell design carries two opposing constraints: leaving no gaps requires overlap, and not
  producing interference requires a reuse distance for the same channel.
- At budget 58, 52, 46, and 40, associated is **31, 23, 17, 13**; false admit is **5, 0, 0, 0**;
  false deny is **1, 4, 10, 14**. Total deviation, **6, 4, 10, 14**, is lowest at 52.
- Growing the budget sends two-thirds of the gain outside the cell; growing the apparatus does
  not bring it closer to intent.
- Three configurations that zero out the leak look identical in one column; in the other their
  shortfalls are **4**, **10**, and **14**. Counting one direction hides the other.
- When the interference ceiling drops to 4, intent is met exactly (0 false admit, 0 false deny);
  when the ceiling drops to 0, the cell grows and 5 clients leak in. Interference is part of the
  boundary.

#### Next Step

Up to this point every client was counted as belonging to a single point and never moved from
its spot. Yet overlap was built in on purpose: a client at the edge hears two points at once,
and as it walks, one weakens while the other strengthens. Then the association decision is made
not once but **repeatedly**. The next lesson measures when the handover should happen: raising
the handover threshold lets the client go early, lowering it makes it stick to a weak point.
Early handover and late handover are two ends of the same dial, and they are **two separate
errors**.

### Roaming and Band Steering

Source: https://academia.sh/en/courses/wireless-and-security/roaming-and-band-steering

As the handover threshold drops from 30 to 14, false admit falls from 8 to 0 and false deny rises from 0 to 8; early handover and late handover are two ends of the same dial, and fixing one direction grows the other.

The previous lesson overlapped cells on purpose: so no gap would remain, neighboring cells'
edges were deliberately overlapped. The measurement swept the budget and the clients stood
still — none of the forty clients moved.

Overlap was built exactly for those who move. A client at the edge hears two points at once; as
it walks, one weakens and the other strengthens. Then the association decision is made not once
but repeatedly, and every time the same question is asked: should I let go of what I have? What
this lesson measures is the threshold of that question. Raising the threshold lets the client go
early; lowering it makes it stick to a weak point. Both are errors, and **they do not show up in
the same numbers.**

#### Who Makes the Handover Decision

**Roaming** is a client moving its bond from one access point to another. The first thing that
has to be said is who holds the decision: the decision **belongs to the client**. The network
can eject a client from a point, tell it about its neighbors, even suggest that it go to a
specific point; but the client itself chooses which point to associate with.

This directly explains why the measurement is two-directional. The only thing in the network's
hands is **thresholds**: below which signal strength the client is told "I'm not holding you
anymore," what gets reported in the neighbor list. The threshold is what the network writes; the
decision is what the client makes.

```text
# taught handover sequence , fictional and not executed

1. point A -> client        neighbor report
                              A: channel 4 , measured signal 21
                              B: channel 1 , measured signal 29
                              C: channel 7 , measured signal 12

2. client                   decision
                              signal in hand 21 , threshold 22 -> let go

3. client -> point B        association request

4. point B -> client        association response
                              result: accept , granted tier 4

5. point A                  old bond dropped
                              <-- frame loss occurs between step 2 and step 3
```

The note under the fifth step is roaming's real cost. Between two associations, the client is
bound to no point at all; frames arriving in that interval drop. If the interval is short, upper
layers absorb it as a latency ripple; if it is long, the connection looks broken. There are other
things that have to be re-established during a handover, and one of them is the subject of the
next lesson.

The first step also has a cost, and it is less visible. If the client does not know its
neighbors, it has to find them itself; finding them means listening to other channels. While it
listens to a channel, it is not on its own channel — meaning it misses frames arriving at that
moment. **Scanning produces loss even when no handover happens.** The more often a client scans,
the fresher its knowledge of neighbors, and the more frames it misses.

A **neighbor report** exists exactly to ease this trade-off: the network hands the client the
list it should search. The client now listens to the three points reported instead of ten
channels. But who writes the report is the previous lesson's question — an independent access
point does not know its neighbors, and without a shared view there is no list either. This is
where roaming's quality connects back to where the decision is made.

#### Early Handover and Late Handover

The measurement's fiction is a **corridor**: two access points stand 50 m apart, and every client
sits somewhere along this line. The client's distance to A is the first lesson's `distance`
field; its distance to B is `50 - distance`.

Intent, in its plainest form, is this: the client should belong to whichever point it is closer
to. The overlap from the previous lesson creates an ambiguity here — a client between 20 and
30 m is counted inside both cells. For the oracle to give a single answer, the nearest-point rule
is chosen; the corridor's midpoint, 25 m, is the boundary.

The mechanism, though, does not know what intent can know. The client does not measure its own
position; it only measures the signal strength of the point it currently holds, and lets go when
that strength drops below the threshold. Because signal strength depends on both distance **and**
interference, two clients at the same distance let go at different times.

Two errors follow from this:

- **Early handover** — the client lets go while still close to A. Intent would have held it back,
  the mechanism let it through: **false admit**.
- **Late handover** — the client sticks to A while already close to B. Intent would have let it
  through, the mechanism held it back: **false deny**.

#### What Band Steering Does Not Know

**Band steering** is a second handover decision: should the client be placed on the wide band,
whose range is short but whose tier ceiling is high, or the narrow band, whose range is long but
whose ceiling is low?

The right answer depends on what the client would actually get on the wide band. The problem is:
**the client has not tried the wide band yet.** When the steering decision is made, only the
narrow-band reading is in hand, and the decision is made from that reading alone. The rule is a
**proxy measurement** — it does not measure what it wants to measure, it measures something else
correlated with it.

The measurement's assumptions:

- **WN22.** Forty clients; distance and interference come from the first lesson's fiction. The
  signal budget is fixed at 52 in the roaming measurement.
- **WN23.** The corridor fiction: two points stand 50 m apart; the client's distance to B is
  `50 - distance`. Intent is the nearest-point rule; the boundary is 25 m.
- **WN24.** The mechanism sees only the signal strength of the point it currently holds.
  Position, B's signal, and the client's direction of travel do not enter the measurement.
- **WN25.** The swept handover thresholds are 30, 26, 22, 18, and 14. Only the threshold changes
  at a time.
- **WN26.** In the ping-pong measurement, the signal is assumed to swing by **3 units** over a
  short time. A client is counted as unstable if its swing interval crosses both the drop
  threshold and the return threshold; at hysteresis margin 0 the two thresholds coincide.
- **WN27.** In band steering, the narrow band is modeled with budget 58, the wide band with
  budget 46; the wide band's tier ceiling is double (`2 × tier`). This is the fiction of the
  range–speed trade-off.
- **WN28.** Band intent comes from the oracle: if a client would truly get more on the wide band,
  it belongs there. The oracle knows both bands' tiers.
- **WN29.** The steering rule sees only the narrow-band reading. The wide-band reading does not
  enter the decision; if it did, this would be a measurement, not a rule.
- **WN30.** The set is forty clients; the smallest measurable difference is **1/40 = 0.025**.

#### Measurement

```python
"""Roaming and band steering: early handover and late handover are two separate errors."""
SEED = 20260811
POWER = 52
CORRIDOR = 50        # distance between the two access points, meters
SWING = 3             # the range the signal swings over a short time
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))


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

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


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def signal(c, power=POWER):
    return power - c["distance"] - c["interference"]


def rate_tier(c, power=POWER):
    for threshold, tier in TIERS:
        if signal(c, power) >= threshold:
            return tier
    return 0


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def handover_intent(c):
    """The client should belong to whichever point it is closer to."""
    return c["distance"] > CORRIDOR / 2


def handover(c, threshold):
    """Mechanism: if the signal from the point in hand drops below the threshold, let go."""
    return signal(c) < threshold


def unstable(c, threshold, margin):
    """If the signal's swing crosses both thresholds as it moves, the client ping-pongs."""
    return threshold + margin - SWING <= signal(c) <= threshold + SWING


pool = clients()
print(f"clients {len(pool)} | corridor {CORRIDOR} m | should belong to B "
      f"{sum(1 for c in pool if handover_intent(c))}")
print()

print(f"{'threshold':>9s} {'crossed':>7s} {'correct':>7s} {'false admit':>12s} "
      f"{'false deny':>12s}")
for threshold in (30, 26, 22, 18, 14):
    d = gap(pool, handover_intent, lambda c, t=threshold: handover(c, t))
    print(f"{threshold:9d} {d['correct_admit'] + d['false_admit']:7d} "
          f"{d['correct_admit'] + d['correct_deny']:7d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d}")
print()

print(f"{'hysteresis margin':>17s} {'unstable clients':>17s}")
for margin in (0, 2, 4, 6):
    print(f"{margin:17d} {sum(1 for c in pool if unstable(c, 22, margin)):17d}")
print()

NARROW, WIDE = 58, 46


def rate_narrow(c):
    return rate_tier(c, NARROW)


def rate_wide(c):
    """Wide band: short range, double the tier ceiling."""
    return 2 * rate_tier(c, WIDE)


def band_intent(c):
    return rate_wide(c) > rate_narrow(c)


def steering(c, threshold):
    """Decision is made from the narrow-band reading alone; the wide band has not been tried yet."""
    return signal(c, NARROW) >= threshold


print(f"{'steering threshold':>19s} {'pushed to wide':>14s} {'correct':>7s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for threshold in (44, 40, 36, 32):
    d = gap(pool, band_intent, lambda c, t=threshold: steering(c, t))
    print(f"{threshold:19d} {d['correct_admit'] + d['false_admit']:14d} "
          f"{d['correct_admit'] + d['correct_deny']:7d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d}")
print()
print(f"clients that truly belong on the wide band: {sum(1 for c in pool if band_intent(c))}")
print("tier lost by false admits (no, narrow tier, wide tier):")
print([(c["no"], rate_narrow(c), rate_wide(c)) for c in pool
       if steering(c, 36) and not band_intent(c)])
```

```
clients 40 | corridor 50 m | should belong to B 19

threshold crossed correct  false admit   false deny
       30      27      32            8            0
       26      27      32            8            0
       22      19      36            2            2
       18      17      36            1            3
       14      11      32            0            8

hysteresis margin  unstable clients
                0                 9
                2                 9
                4                 6
                6                 2

 steering threshold pushed to wide correct  false admit   false deny
                 44              7      38            0            2
                 40              9      40            0            0
                 36             13      36            4            0
                 32             13      36            4            0

clients that truly belong on the wide band: 9
tier lost by false admits (no, narrow tier, wide tier):
[(1, 6, 4), (11, 6, 4), (23, 6, 4), (36, 6, 4)]
```

#### The Threshold Shifts the Balance

The top table turns a single dial, and two columns move in opposite directions.

At threshold 30, false admit is 8, false deny is 0. Clients let go while still close to A; eight
of them have crossed to B while still on A's side. No one is late, because the threshold is so
high that everyone leaves ahead of time.

At threshold 14, the opposite: false admit is 0, false deny is 8. No one leaves early, and eight
clients stick to A while already close to B. A client that sticks both keeps its own tier low and
holds the medium for longer; the shared-time calculation from the first lesson shows up here
again.

The three middle rows show where the balance shifts: **2/2**, **1/3**, and, in between, 30 and 26
giving the same row. The two rows coming out identical is a resolution effect of the fiction — no
client's signal strength falls between 26 and 30, so moving the threshold across that interval
changes no decision at all. The measurement band (WN30, **0.025**) cannot see this gap.

The rule to read is: **early handover and late handover are two ends of the same dial.** An
operator who receives only sticking complaints raises the threshold and ends the sticking; the
moment it ends, early handover begins, and that too is a complaint — a different one, because
early handover produces short outages while sticking produces continuous slowness. A measurement
that does not count the two directions separately can do nothing but move the dial from one end
to the other.

#### Ping-Pong and Hysteresis

The middle table counts a different flaw. Signal strength is not constant; it swings by a few
units over a short time (WN26). A client sitting right at the threshold's edge makes the decision
to let go because of this swing, then returns, then lets go again. This is called **ping-pong**,
and it produces a frame loss on every round.

In the single-threshold mechanism (hysteresis margin 0), 9 of the forty clients sit inside this
band. Raising the margin to 4 drops the unstable clients to 6, to 6 drops it to 2. The mechanism
is this: the drop threshold and the return threshold are separated; a client lets go only when
the signal falls below the lower threshold, and returns only when it rises above the upper one.
The gap between them absorbs the swing.

The margin's cost is read in the table above. Holding the drop threshold fixed and raising the
return threshold makes returning harder: a client that went to the wrong point stays there
longer. **Hysteresis reduces instability and lengthens the life of a wrong decision.** Margin 2
changing nothing is the same resolution problem — a 2-unit margin sits below a 3-unit swing and
removes no client from the band.

#### A Decision Made from a Wrong Reading

The bottom table measures band steering. Of the forty clients, 9 truly belong on the wide band;
the steering rule tries to guess this from the narrow-band reading.

At threshold 44 the rule pushes 7 clients: false admit 0, false deny 2. The rule is cautious; it
sends no one to the wrong place, but it fails to run two clients on the band they deserve. At
threshold 36, 13 clients are pushed: false admit 4, false deny 0. These four are listed in the
last row and their cost is plain — they would have gotten tier 6 on the narrow band and get 4 on
the wide band. The rule slows down exactly the clients it pushed to "improve."

The threshold 40 row gives 40/40 correct: 0 false admit, 0 false deny. This row has to be read
carefully. It does not show that the rule is correct; it shows that, in this set, that particular
cut happens to coincide with intent. The rule still does not measure what it wants to measure —
it never sees the wide-band reading (WN29). The same threshold could well be wrong on a
different client distribution, because a proxy measurement's accuracy depends on the set it is
measuring.

And threshold 36 and 32 giving the same row is the counterpart of the 30–26 pair in the table
above: there are intervals where moving the threshold changes no decision at all. It cannot be
assumed that every time a threshold is "adjusted," something has actually changed.

#### Summary

- The client makes the roaming decision; the only mechanism in the network's hands is
  thresholds. The measure is therefore the gap between the network's intent and the client's
  decision.
- As the handover threshold drops from 30 to 14, false admit is **8, 8, 2, 1, 0**, false deny is
  **0, 0, 2, 3, 8**. Early handover and late handover are two ends of the same dial.
- In the single-threshold mechanism, 9 clients sit in the ping-pong band; at hysteresis margins
  4 and 6 this drops to 6 and 2, but the life of a wrong decision lengthens.
- The band steering rule never sees the wide band's reading; at threshold 44, 2 clients do not
  go to the band they deserve, at threshold 36, 4 clients are dropped from tier 6 to tier 4.
- There are intervals where changing the threshold changes no decision (30–26 and 36–32); that
  an adjustment was effective cannot be inferred from the adjustment having been made.

#### Next Step

Up to this point, the medium determined who got through. Distance, interference, budget, and
threshold — all four were physical or numerical quantities, and none of them asked who the
client was. Everyone standing inside the cell associated, no one standing outside could
associate; distance was what drew the boundary.

So what happens if an **identity condition** is added to the crossing? Then association passes
two tests: is the client close enough, and is the client acceptable? The next lesson builds this
second test and counts the same two directions there too — the identity condition admits some
clients it should not, and denies some it should not.

### Wireless Security Protocols

Source: https://academia.sh/en/courses/wireless-and-security/wireless-security-protocols

Generation selection looks like a security setting but behaves like a coverage setting: in the same forty-client set, as the generation condition rises false admit falls from 5 to 2, false deny rises from 1 to 20, and moving from mixed mode to a single generation drops 13 clients.

Throughout the previous lessons, one thing alone decided who got through: **the medium**. If
signal strength is above the threshold the client associates, below it the client cannot. What
drew the boundary was distance, interference, and the access point's power; none of it asked who
the client was.

This lesson's question is: if an **identity condition** is added to the crossing, what happens to
the boundary? Wireless **security generations** do exactly this — they tie association to a
second condition alongside the medium. The generations' security properties, their authentication
methods, and the attacks aimed at these mechanisms were measured in the Cybersecurity
curriculum's Network Security course and are not repeated here. What is measured here is
something else: what the generation condition does to **association and coverage**.

#### Association's Second Condition

A client's **association** with an access point is passing two separate tests. The first is the
medium's, and it was measured in the previous lessons: signal strength has to be above the
association threshold. The second is the identity's: the client has to be able to implement at
least one of the generations the access point offers.

The two conditions are joined by **and**. If the medium is sufficient but the generation is not
supported, there is no association; if the generation is supported but the signal is weak, there
is again none. This means the boundary narrows a second time. The first narrowing is
continuous — signal falls with distance. The second is discrete: the client either supports it or
does not, there is no value in between.

This distinction determines the whole measurement. When the medium condition changes, the
boundary **shifts**; when the generation condition changes, the boundary is **punctured** — a
client at the exact center of the cell, receiving the strongest possible signal, can still be
left outside.

#### What a Generation Packages

A generation is not a single setting; it is a **bundle** that changes together. Four things sit
inside the bundle: how the client identifies itself (**authentication mode**), which **cipher
suite** protects the frames carried, how session keys are derived, and whether management frames
get integrity protection.

Authentication mode splits into two classes. In **pre-shared key mode** everyone who enters the
network carries the same credential; the access point knows not who arrived, but that the correct
key arrived. In **enterprise mode** each client has its own credential, and authentication is
asked of a separate authentication server; the access point does not make the decision itself, it
carries it. The second adds a round trip to association, and if the server is unreachable,
association never happens at all.

The access point announces what it offers; the client picks, from the announcement, the
generation that fits it.

```text
# taught dump , not executed
# generation numbers, suite names, and field values are fictional

access point announcement
  network name                 : cell-b.example
  offered generation           : 2, 3
  authentication mode          : generation 2 pre-shared key, generation 3 enterprise
  cipher suite                 : generation 2 suite-b, generation 3 suite-c
  management frame protection  : generation 3 required, generation 2 optional

client -> access point
  association request
    selected generation        : 2
    cipher suite                : suite-b
```

This dump is a taught layout and has not been executed; the measurement's numbers do not come
from here but from the run below.

#### Why an Old Generation Gets Dropped

Dropping a generation is not a fashion change. A generation is dropped because it **could not
keep the promise it made.** The oldest generations' promise was: a frame carried over the
wireless medium will be as protected as a frame carried over a cable. The promise cracked in two
places. The key material was short and was not refreshed often enough from frame to frame, so
once enough traffic accumulated, the protection weakened statistically. Second, integrity
checking was not tightly bound to the frame's content; a modified frame was not guaranteed to be
noticed.

**How** these two flaws are exploited is not this course's subject and is not written here; key
recovery, handshake capture, and trial procedures live in the Cybersecurity curriculum's Network
Security course, in an authorized-testing context. The only inference here is: a generation is
dropped the moment it cannot keep its promise, and dropping it means **tightening the association
condition**. The cost this lesson measures is exactly that.

#### Mixed Mode

Deployments that want to keep old clients on the network offer two generations at once. This is
called **transition mode** or **mixed mode**. The intuition is: the new client picks the new
generation, the old client picks the old one, no one is left out.

This is the point where the intuition needs measuring. In mixed mode, the set of clients that can
associate is the set for the **lowest** of the offered generations — because everyone who
supports the lowest one gets in. This means coverage, too, is the lowest generation's coverage.
Mixed mode does not add the two generations' coverage together; it gives you the lower one's.

The measurement's assumptions:

- **WN51.** Forty clients are produced from the shared fiction; distance and interference values
  are unchanged. The oracle is intent itself: the client inside the thirty-meter cell should
  receive service.
- **WN52.** The access point's power is fixed at 58 throughout the measurement. The only thing
  this lesson changes is the generation condition; power, radius, and interference are held
  constant.
- **WN53.** There are three generations, and they are fictional. Every client has a **ceiling**:
  a client with ceiling 3 implements all three generations, one with ceiling 1 implements only
  the first. Ceilings are produced from a separate seed and are independent of distance.
- **WN54.** The association condition is **and**: the medium has to be sufficient, **and** the
  client's ceiling has to reach the lowest of the offered generations.
- **WN55.** Mixed mode means the offered generation set has more than one member. To be able to
  associate it is enough for the ceiling to reach the set's lowest member; once associated, the
  client uses the highest offered generation its ceiling reaches.
- **WN56.** The count of generation actually used is taken over associated clients; a client that
  cannot associate uses no generation and does not enter this count.
- **WN57.** The set's resolution is forty clients; the smallest measurable difference is
  **1/40 = 0.025**. No difference smaller than this is claimed.

#### Measurement

```python
"""Generation selection: its effect on association and coverage.

Part 1 - across four generation regimes: associated, full rate, false admit, false deny.
Part 2 - clients dropped moving from mixed mode to a single generation split two ways.
"""
SEED = 20260811
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))
POWER = 58
CEILINGS = (1, 1, 1, 2, 2, 2, 2, 3, 3, 3)
REGIMES = (("generation 1 only", (1,)), ("generation 2 only", (2,)),
           ("generation 3 only", (3,)), ("mixed 2+3", (2, 3)))


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

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


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def signal(c, power=52):
    return power - c["distance"] - c["interference"]


def rate_tier(c, power=52):
    for threshold, tier in TIERS:
        if signal(c, power) >= threshold:
            return tier
    return 0


def cell_intent(c, radius=30):
    return c["distance"] <= radius


def medium(c, power=52):
    return rate_tier(c, power) > 0


def capability(pool, seed=SEED + 7):
    r = generator(seed)
    for c in pool:
        c["ceiling"] = CEILINGS[r(10)]
    return pool


def associates(c, offered, power=POWER):
    return medium(c, power) and c["ceiling"] >= min(offered)


def used_generation(c, offered):
    """The highest offered generation the ceiling reaches once associated."""
    return max(g for g in offered if g <= c["ceiling"])


pool = capability(clients())
print(f"clients {len(pool)} | inside cell {sum(cell_intent(c) for c in pool)} | "
      f"ceiling 1 {sum(c['ceiling'] == 1 for c in pool)} | "
      f"ceiling 2 {sum(c['ceiling'] == 2 for c in pool)} | "
      f"ceiling 3 {sum(c['ceiling'] == 3 for c in pool)}")
print()
print(f"{'regime':<18s} {'associated':>10s} {'full rate':>9s} {'false admit':>12s} "
      f"{'false deny':>12s}")
for name, offered in REGIMES:
    d = gap(pool, cell_intent, lambda c, o=offered: associates(c, o))
    full = sum(1 for c in pool if associates(c, offered) and rate_tier(c, POWER) == 6)
    print(f"{name:<18s} {d['correct_admit'] + d['false_admit']:10d} {full:9d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")
print()
dropped = [c for c in pool if associates(c, (2, 3)) and not associates(c, (3,))]
print(f"mixed 2+3 -> generation 3 only: dropped {len(dropped)}, "
      f"inside cell {sum(cell_intent(c) for c in dropped)}, "
      f"outside cell {sum(not cell_intent(c) for c in dropped)}")
linked = [c for c in pool if associates(c, (2, 3))]
print("generation actually used in mixed 2+3 regime: " + ", ".join(
    f"generation {g}: {sum(1 for c in linked if used_generation(c, (2, 3)) == g)}"
    for g in (2, 3)))
```

```
clients 40 | inside cell 27 | ceiling 1 12 | ceiling 2 17 | ceiling 3 11

regime             associated full rate  false admit   false deny
generation 1 only          31        13            5            1
generation 2 only          22         8            4            9
generation 3 only           9         3            2           20
mixed 2+3                  22         8            4            9

mixed 2+3 -> generation 3 only: dropped 13, inside cell 11, outside cell 2
generation actually used in mixed 2+3 regime: generation 2: 13, generation 3: 9
```

#### Two Directions of Raising the Generation

The first row is a check point: because generation 1 stays below everyone's ceiling, the identity
condition eliminates no one, and the table reproduces the shared fiction's power-58 row exactly —
**31** associated, **13** full rate, **5** false admit, **1** false deny. This is the state with
no generation condition added.

As the condition tightens, two columns move in opposite directions. False admit falls from 5 to
4, then to 2: some of the clients that stayed outside the cell yet associated drop out because
they cannot implement the newer generation. False deny, on the other hand, rises from 1 to 9,
then to 20. Against a total of **3** units of leakage gained over three steps stands **19** units
of service lost. Both differences sit well above the 0.025 resolution.

This is the second claim being paid a second time on the wireless side. Tightening the generation
condition is made as a security decision, but its result is a **coverage** decision: twenty of
the twenty-seven-strong inside-cell population cannot associate at all under the generation 3
regime. An audit that counts only false admit reports this regime as an improvement and never
sees the other direction.

The third column carries a separate warning. Of the nine clients associated under the generation
3 regime, only 3 are at the full rate tier. Associating is not the same as working; the generation
condition narrows the association gate but never touches rate tiers at all, because what
determines the tier is still signal strength.

The last two rows are mixed mode's measure. All four columns of the mixed 2+3 regime are
identical to generation 2 only: **22, 8, 4, 9**. Mixed mode ties coverage not to the higher
generation but to the lowest offered generation. The cost of closing mixed mode and moving to
generation 3 only is **13** clients, and that thirteen splits into two directions: **11** are
inside the cell and get added to false deny, **2** are outside and get subtracted from false
admit. Saying "13 clients dropped" as a single number would put these two directions on the same
scale.

#### The Generation Actually Used in Mixed Mode

The association condition looks at the set's lowest member; the generation used looks at its
highest. Because the two questions are separate, they are counted separately. The last row gives
this: of the 22 clients associated under the mixed regime, 13 run over generation 2, 9 over
generation 3. More than half the associated population sits on the lower generation, and this
number is the same set as the thirteen clients that would drop when mixed mode is closed — we are
counting the same clients as the answer to two different questions.

The importance of this row is: a network "supporting generation 3" does not show that traffic on
that network runs under generation 3 protection. If the announcement carries two generations at
once, which client uses which is the result of the ceiling distribution and can only be read from
association records. The measurement shows this distribution on its own: of the 17 clients with
ceiling 2, the 13 that can associate stay on the lower generation; of the 11 clients with ceiling
3, the 9 that can associate move up to the higher one.

The authentication mode's round trip stands behind this row too. In pre-shared key mode, the
decision ends at the access point. In enterprise mode, the access point carries the decision and
asks an authentication server; a round trip is added to association, and if the server is
unreachable, association never completes at all. In this case, the medium condition being
satisfied rescues nothing — signal strength is full, the client is inside, and association still
does not happen. This is what it means for the generation condition to "puncture" the boundary.

#### Surfaces and Narrowings

The surfaces generation selection opens up are not an attack narrative; each is a **configuration
result**. Next to every surface counted stands the setting that narrows it.

**First surface — mixed mode's floor effect.** In mixed mode the network's coverage is the lower
generation's; the protection the lower generation carries is also the protection actually used on
the network. In the measurement this shows up as the mixed row coming out identical to the
generation 2 row. **Narrowing:** the two generations are not offered under the same network name;
clients that require the lower generation are put on a separate network name and a separate
segment, so the floor effect stays confined to that segment instead of the whole network. A
second narrowing is closing the lower generation entirely, and its cost has been measured: **13**
clients, 11 of them from inside the cell.

**Second surface — a shared credential.** In pre-shared key mode everyone carries a single
credential; a departing client's access can only be cut by changing the key across the entire
network. **Narrowing:** enterprise mode splits the credential per client, and a single client's
access can be cut without touching the rest of the network. The cost is the round trip added to
association and the dependency created on the authentication server.

**Third surface — unprotected management frames.** The frames that open and close association
carry no integrity protection under older generations; there is no mechanism checking these
frames' correctness. **Narrowing:** management frame protection binds these frames to integrity
checking too, and it is mandatory under the higher generation. In the measurement this is
obtained only by raising the generation condition; so the cost of this narrowing is, again, the
false deny column.

**Fourth surface — the invisibility of generation selection.** Which generation a client picks
depends on the announcement and the client's ceiling; a network administrator does not get to
assume how many clients enter on the lower generation. **Narrowing:** counting the selected
generation in association records — this is the counterpart of the measurement's ceiling
distribution: 12 clients have ceiling 1, 17 have ceiling 2, 11 have ceiling 3.

#### Summary

- Association is the **and** of two conditions: the medium condition shifts the boundary, the
  generation condition punctures it; a client at the cell's center can be left outside despite a
  strong signal.
- A generation is not a single setting but a bundle: authentication mode, cipher suite, key
  derivation, and management frame protection change together.
- As the generation condition tightens, false admit goes **5 → 4 → 2**, false deny goes
  **1 → 9 → 20**; three units of leakage gained are paid for with nineteen units of service lost.
- Mixed mode ties coverage to the lowest offered generation; all four of its columns match that
  generation's, and the cost of closing mixed mode is 13 clients — 11 from inside the cell, 2
  from outside.
- Of the nine clients associated under the generation 3 regime, only three are at the full rate
  tier; the generation condition narrows the gate, it does not change the tier.

#### Next Step

Everything measured up to this point stayed inside a single range class: a thirty-meter cell,
distances in the tens of meters, a rate that steps down tier by tier. Yet the same intent — "the
client inside the boundary receives service" — is also built into a pairing bond that spans a
few meters, and into a low-power bond stretching for kilometers. The next lesson measures the
same forty clients across three separate range classes: as range grows, where does the rate tier
land, where does false deny go, and what does intent's promise of "service" correspond to in each
class?

### Short-Range and Wide-Area Wireless

Source: https://academia.sh/en/courses/wireless-and-security/short-range-and-wide-area-wireless

The same intent is measured across three range classes: as range grows the highest rate tier reached falls from 8 to 6, then to 2, associated clients rise from 11 to 37, and the sign of the error comes not from the class's range but from how the radius is tuned against the power budget.

The previous lesson added an identity condition to association and counted what it cost
coverage. But the whole measurement stayed at a single scale: a thirty-meter cell, distances in
the tens of meters, a rate-tier ladder that climbs up to six. This scale is not all of wireless.

The same intent — "the client inside the boundary receives service" — is also built into a
pairing bond spanning a few meters, and into a low-power bond stretching for kilometers. This
lesson's question is: with intent's form held fixed, what does **scale itself** change? As range
grows, what happens to the rate tier, where does false deny go, and is "receiving service" the
same thing in every class?

#### The Three Classes of Range

Wireless bonds fall into three classes according to the area they intend to cover. The classes
are not upgraded versions of one another; they are different answers to different intents.

**Short range.** Covers an area of a few meters and is mostly built between two ends: a
peripheral device and the host that uses it, a sensor and the device that reads it. The topology
is usually a **pairing** pair, not a cell. The power budget is small because the device runs on a
battery; but because the distance is also small, signal strength stays high and the rate tier can
climb. **Pairing** is this class's own particular step: before the bond is built, the two ends
recognize each other once.

**Local range.** This is the previous lessons' cell: one access point, a radius in the tens of
meters, many clients. The medium is shared, interference is a measurable cost, and the rate tier
steps down with distance.

**Wide area.** Bonds that stretch for kilometers and run on low power. This class's solution is
to buy range with speed: it works over a narrow band, with small payloads, and infrequent
transmission. A device sends a few tens of bytes of measurement a few times a day and stays
silent in between. This is the **duty cycle** — the ratio of the time the device spends on the
medium to total time. A sparse duty cycle protects both the battery and the medium.

#### What Trades for What

All three classes deal with the same three quantities: **range**, **rate tier**, and **power
budget**. What is taken from one end is given to the other.

There are two ways to grow range. The first is raising power, and its cost was counted in the
previous lessons: as power rises, the number of clients leaking outside the cell also rises. The
second is **thinning out** the information carried — sending the same information over a longer
time, in a narrower band, using fewer signal levels. A thinned-out signal can be pulled out from
under the noise; in exchange, the number of bits carried per unit time falls. This is why the
wide-area class's rate-tier ceiling is low.

The classes' distinguishing fields can be collected into a table. The values below are
**fictional** and report no real band allocation, channel plan, or regulatory rule.

```text
# taught dump , not executed
# band names, channel counts, and payload limits are fictional

class          band     channels  frame payload    duty cycle   topology
short range    band-a          6  up to 256 bytes   continuous   pairing pair
local range    band-b         12  up to 1500 bytes  continuous   cell
wide area      band-c          3  up to 64 bytes    sparse       star
```

The frame payload column produces a conclusion on its own: in the wide-area class, a message that
does not fit in sixty-four bytes cannot be sent. An application running on top of this class has
to design its message against this limit — protocol choice comes bundled with range choice.

#### Same Intent, Three Scales

Intent's **form** is the same across all three classes: a radius is drawn and "the client inside
receives service" is declared. What changes is the radius's number. The short-range class
promises twelve meters, local range thirty, wide area forty.

This is what keeps the measurement fair. Every class is audited against its **own** promise; a
short-range bond is not counted flawed for failing to cover thirty meters. False admit and false
deny are computed against the class's own radius.

The measurement's assumptions:

- **WN58.** Forty clients are produced from the shared fiction; distance and interference values
  are the same across all three classes. The only thing that changes is the class itself.
- **WN59.** Each class is defined by three numbers: power budget, intent radius, and rate-tier
  ladder. The local-range class's ladder comes from the shared fiction; the other two ladders and
  the power values are fictional.
- **WN60.** In every class the oracle is that class's own radius: 12 for short range, 30 for
  local, 40 for wide area. Intent's form stays the same, only its number changes.
- **WN61.** Association means reaching the ladder's lowest rung; a client whose tier comes out
  zero cannot associate.
- **WN62.** Tier numbers are **relative** speed steps; they carry no unit and can be summed
  within a class. Because the ladders are fictional, comparing totals across classes is read as
  an ordering of magnitude, not a ratio.
- **WN63.** The interference value is independent of class and lies between 0 and 12. In classes
  with a small power budget, the same interference eats a larger share of the budget.
- **WN64.** The set's resolution is forty clients; the smallest measurable difference is
  **1/40 = 0.025**. When a class's within-intent population is smaller than forty, no finer
  difference is claimed over that subset.

#### Measurement

```python
"""Same intent in three range classes.

Part 1 - each class's associated, highest tier reached, two-directional error.
Part 2 - tier distribution and the same client's tier across the three classes.
"""
SEED = 20260811
# (name, power, radius, rate-tier ladder) -- ladders and power values are fictional
CLASSES = (
    ("short range", 40, 12, ((30, 12), (26, 8), (20, 4))),
    ("local range", 52, 30, ((34, 6), (28, 4), (22, 2), (18, 1))),
    ("wide area", 70, 40, ((46, 2), (24, 1))),
)


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

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


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def cell_intent(c, radius=30):
    return c["distance"] <= radius


def tier(c, power, ladder):
    signal = power - c["distance"] - c["interference"]
    for threshold, k in ladder:
        if signal >= threshold:
            return k
    return 0


pool = clients()
print(f"clients {len(pool)} | distance {min(c['distance'] for c in pool)}"
      f"-{max(c['distance'] for c in pool)} | interference "
      f"{min(c['interference'] for c in pool)}-{max(c['interference'] for c in pool)}")
print()
print(f"{'class':<12s} {'radius':>6s} {'in intent':>9s} {'associated':>10s} "
      f"{'top':>3s} {'at top':>6s} {'total':>6s} {'per client':>10s} "
      f"{'false admit':>12s} {'false deny':>11s}")
for name, power, radius, ladder in CLASSES:
    tiers = [tier(c, power, ladder) for c in pool]
    d = gap(pool, lambda c, r=radius: cell_intent(c, r),
            lambda c, p=power, l=ladder: tier(c, p, l) > 0)
    top = max(tiers)
    admitted = d["correct_admit"] + d["false_admit"]
    print(f"{name:<12s} {radius:6d} {sum(cell_intent(c, radius) for c in pool):9d} "
          f"{admitted:10d} {top:3d} "
          f"{tiers.count(top):6d} {sum(tiers):6d} "
          f"{sum(tiers) / admitted:10.2f} "
          f"{d['false_admit']:12d} {d['false_deny']:11d}")
print()
print("tier distribution (0 means unable to associate)")
for name, power, radius, ladder in CLASSES:
    count = {}
    for c in pool:
        k = tier(c, power, ladder)
        count[k] = count.get(k, 0) + 1
    print(f"  {name:<12s} " + "  ".join(f"tier {k}: {count[k]:2d}"
                                         for k in sorted(count, reverse=True)))
print()
print("the same client in three classes")
print(f"  {'no':>3s} {'distance':>8s} {'interference':>12s} "
      + " ".join(f"{name:>12s}" for name, _, _, _ in CLASSES))
for no in (3, 11, 38, 26):
    c = pool[no - 1]
    print(f"  {c['no']:3d} {c['distance']:8d} {c['interference']:12d} "
          + " ".join(f"{tier(c, p, l):12d}" for _, p, _, l in CLASSES))
```

```
clients 40 | distance 6-44 | interference 0-12

class        radius in intent associated top at top  total per client  false admit  false deny
short range      12         8         11   8      7     72       6.55            3           0
local range      30        27         23   6      9     88       3.83            0           4
wide area        40        38         37   2     13     50       1.35            1           2

tier distribution (0 means unable to associate)
  short range  tier 8:  7  tier 4:  4  tier 0: 29
  local range  tier 6:  9  tier 4:  4  tier 2:  8  tier 1:  2  tier 0: 17
  wide area    tier 2: 13  tier 1: 24  tier 0:  3

the same client in three classes
   no distance interference  short range  local range    wide area
    3       13            0            8            6            2
   11       18            2            4            4            2
   38       31           11            0            0            1
   26       44            1            0            0            1
```

#### What Falls as Range Grows

The `top` column is the lesson's main measure: the highest rate tier actually reached in each
class is **8**, **6**, and **2**. As range grows the ceiling falls, and this is not a
configuration flaw but the trade-off itself. The `at top` column next to it moves the other way:
the number of clients at the top tier is **7**, **9**, and **13**. The wide-area class lifts a
larger share of its population to its own ceiling — but that ceiling is very low.

The `associated` and `total` columns have to be read together. The wide-area class reaches **37**
clients, the highest of the three; the tier total it carries is **50**, the lowest of the three.
The local-range class reaches only **23** clients but carries **88**. Short range carries **72**
with **11** clients. **The class that reaches the most clients carries the least speed.** Because
the ladders are fictional, this comparison is an ordering of magnitude, not a ratio.

The `per client` column is their quotient, and it falls in a single direction as range grows:
**6.55, 3.83, 1.35**. Reaching more clients does not grow the carried total; it compresses the
share. This column shows that choosing a class is as much a **density** decision as it is a
coverage decision.

The distribution table says the same thing once more. In the wide-area class, 24 of forty clients
sit at the lowest tier; only three cannot associate at all. In the local-range class, 17 clients
cannot associate at all, but nine of those who do are at the top tier. The two classes have chosen
two different things: one chose leaving no one out, the other chose serving whoever it reaches
fast.

The last table shows this client by client. Client 3 is at thirteen meters with no interference:
it gets tier 8 on short range, 6 on local, 2 on wide area — the same client, the same spot, three
different speeds. Client 26 is at forty-four meters: it cannot associate at all in the two nearer
classes, and bonds at tier 1 on wide area. Client 38 is at thirty-one meters but its interference
is 11; the local-range class's budget cannot absorb this interference and the client counts as
outside the cell, while the wide-area budget can absorb it.

#### The Sign of the Error Is the Result of the Tuning

The two error columns do not move in a single direction with range, and this is the lesson's
second result.

**Short range: 3 false admit, 0 false deny.** This class keeps its promise and then some. All
eight of the eight clients inside the twelve-meter radius associate, and on top of that three more
clients from outside the radius get in. The power budget is generous relative to the given
radius.

**Local range: 0 false admit, 4 false deny.** This class runs the other way: no leakage at all,
but it cannot serve four clients inside its own cell. The budget is tight relative to the given
radius.

**Wide area: 1 false admit, 2 false deny.** Both directions are small and balanced.

The pattern is: **the sign of the error comes not from the class's range but from how the radius
is tuned against the power budget.** The same class could be moved from false admit to false deny
by growing its radius by two meters. Choosing a range class does not resolve this error; it only
sets **at which scale** it will happen. This is why a comparison that reports only one direction
ranks all three classes wrong: looking at false admit makes short range look worst, looking at
false deny makes local range look worst.

Interference's share also changes by class. The interference value is the same across all three
classes and is at most 12; but that 12 is thirty percent of a forty-unit budget and seventeen
percent of a seventy-unit budget. In small-budget classes interference dominates, in
large-budget classes distance dominates — client 38's situation across the three classes is a
one-line proof of this.

#### Medium Access and How the Identity Condition Takes Shape by Class

The `per client` column raises the next question: how do the thirty-seven clients sharing the
same medium take their turn? The three classes solve this in three different ways.

In short range the bond is built through **pairing**, and the two ends usually follow a schedule;
transmission moments are known in advance, and there is almost no contention. In local range the
medium is shared and the right to transmit is won through contention; as the client count grows,
contention's share grows too. In wide area, contention is not resolved, it is **thinned out**:
the device transmits a few times a day, with a short payload, mostly without agreeing with a
central point first. Collisions happen and the lost transmission is retried; because the sparse
duty cycle keeps collision probability low, this comes cheap.

The previous lesson's identity condition also changes shape by class. In local range the
condition is run again at every association. In short range the condition runs once, at the
moment of **pairing**; the two ends recognize each other at that moment, and later bonds use that
recognition — the condition's round trip has been taken out of association and moved to a
one-time step. In wide area the device is recognized once as it joins the network, and later
transmissions are verified one by one; there is usually no separate step called association. What
the three classes share is this: **the identity condition does not disappear, only which moment
it is placed at changes.**

Topology comes bundled with these three answers too. Short range's pairing pair, local range's
cell around an access point, and wide area's star — all three are names for where medium access
gets resolved.

#### Summary

- The three range classes are not upgraded versions of one another; they are three separate
  answers that trade range, rate tier, and power budget differently.
- As range grows, the highest rate tier reached falls **8 → 6 → 2**; the number of clients at
  the top tier rises **7 → 9 → 13**, because the lower the ceiling, the easier it is to reach.
- The class that reaches the most clients carries the least speed: wide area, 37 clients and 50
  tiers; local range, 23 clients and 88 tiers.
- The sign of the error does not come from range: short range gives 3/0, local range 0/4, wide
  area 1/2; what decides it is how the radius is tuned against the power budget.
- The same client gets three different tiers in the three classes; interference dominates in
  small-budget classes, distance dominates in large-budget ones.

#### Next Step

In the measurement of all three classes, the serving point was singular and never moved from its
spot. The client moved away, its tier dropped, and eventually it could not associate — and no one
offered it a different point. Yet the other way to cover a wide area is not to strengthen a
single point but to line up many points side by side and **hand the client off** between them.
The next lesson builds cellular architecture's core components and measures handover: how much
should two cells' coverage overlap, how do false admit and ping-pong clients grow as overlap
grows, and where does false deny come from as overlap narrows?

### Mobile Networks

Source: https://academia.sh/en/courses/wireless-and-security/mobile-networks

In cellular architecture the network makes the decision, not the client; as the inter-site distance is pulled in from 90 to 30, service's two-directional error falls from 5 and 1 to zero, but overlapping clients rise from 0 to 31, and the handover decision shifts from unnecessary handover to missed handover.

In all three range classes of the previous lesson, the serving point was singular and never
moved from its spot. As the client moved away its tier dropped, eventually it could not
associate, and no one offered it a different point. Yet the other way to cover a wide area is
not to strengthen a single point but to line up many points side by side and **hand the client
off** between them.

This is cellular architecture's founding idea. This lesson's question has two parts: who makes
the handover decision, and **how much should two cells' coverage overlap?** The second question
is a design variable, and it is paid for in both directions at once.

#### Cellular Architecture's Core Components

The architecture splits into two halves. On the **radio access side** stand **base stations**; a
base station carries one or more cells, and every cell has an identity. Cells are neighbors of
one another, and every cell knows its **neighbor list** — handover starts from this list.

On the **core side**, three jobs get done. **Mobility management** keeps track of where every
client is; for a connected client this information is at cell level, for an idle client it is at
**tracking area** level. An idle client's exact cell is not known and does not need to be; when
the client needs to be reached, a page goes out to the whole tracking area. The **gateway** is
where the data plane exits to the outside network. The **subscriber register** holds credentials
and permissions.

On the client side, two components are counted: the **identity module**, which carries the
credential, and the **measurement report**, which reports neighboring cells' signal strength.

```text
# taught layout , not executed
# component names and identity values are fictional

radio access side                  core side
  base station                       mobility management
    cell A (identity 1001)             connected client record
    cell B (identity 1002)             tracking area record
  handover interface                 gateway
    neighbor list                      data plane exit
    context transfer                 subscriber register
                                        credential and permission record

client
  identity module     : carries the credential
  measurement report  : reports neighboring cells' signal strength
```

The real distinction this layout carries is that the **control plane** and the **data plane** are
separate. Handover is a control-plane job: it covers the decision, the transfer of context, and
updating the record. The data plane does not stop in the meantime; the flow is carried from the
old cell to the new one. The two planes being separate is the condition for service not being
interrupted during a handover.

#### Handover

**Handover** is a change of the serving cell, and it is roaming's cellular counterpart. But the
decision flow runs backward. In a wireless local area network, the one that measures and the one
that decides are both the client. In cellular architecture, **the client measures, the network
decides**: the client reports neighboring cells' signal strength through the measurement report,
the serving cell evaluates the report, agrees on context with the target cell, and sends the
handover command to the client.

This reversal has two consequences. First, the decision is gathered in a single hand: the network
can also factor in neighboring cells' load and state; the client cannot see these. Second, the
decision carries a round-trip debt — no handover happens before the report goes out and the
command comes back.

The decision's threshold is the **handover margin**: a handover is not started until the target
cell's signal strength exceeds the serving cell's by this many units. If the margin were zero,
every client whose signal from the two cells was close would be handed back and forth
continuously. In this lesson the margin is held fixed; the only thing changed is **inter-site
distance**.

A handover failing to complete is a separate case. If, after the report is sent, the serving
cell's signal drops below the threshold before the command arrives, the bond breaks; at this
point the client cannot wait for the handover, it starts re-establishing with whichever cell it
finds best and builds context from scratch. This is the exact opposite of the continuity handover
is trying to buy, and it is a late handover decision's real cost. This case does not show up as a
separate column in the measurement; it stands behind the **missed handover** count.

#### How Many Kinds of Handover There Are, and the Idle Client

Not every handover carries the same cost. If the two cells are on the **same** base station,
context is carried inside the station and the core side does not need to know. If the two cells
are on **separate** base stations, context passes through the handover interface and the record
in mobility management gets updated. If the data plane's exit point also changes, one more
operation is needed on the gateway side. Three operations carrying the same name mean three
separate loads on the control plane; this is why the neighbor list determines not only which
cells are neighbors but also **which kind** of handover each will be.

**Sectorisation** directly affects this table. When a base station carries multiple cells through
directional antennas, handovers between those cells fall into the cheap kind; growing the cell
count does not cost the same as spreading cells across separate stations.

The idle client carries a separate balance. A connected client's cell is known; an idle client's
**tracking area** alone is known, and when it needs to be reached a page goes out to the whole
area. If the tracking area is grown, the client does not send a location update unless it changes
area, and the control plane is relieved; in exchange, every page spreads across more cells. If the
area is shrunk, paging gets cheaper and updating gets more expensive. This is another appearance,
inside the architecture, of the two directions the course keeps counting: a setting that cheapens
one direction makes the other more expensive, and a measurement that looks at only one direction
ranks both wrong.

#### Overlap Is a Design Variable

As two cells draw closer to each other, their coverage overlaps. Overlap guarantees leaving no
gap and provides an area where a handover can complete; in exchange, the two cells cover the same
ground a second time, and clients around the midline stay unstable between the two cells.

The measurement's assumptions:

- **WN65.** Clients sit on the line joining cells A and B. Every client's distance to A comes
  from the shared fiction; its distance to B is the absolute value of the difference between the
  inter-site distance and its distance to A.
- **WN66.** The two cells' power is equal, at 58; the design radius is 30 for both. Interference
  is the client's own value and is the same for both cells.
- **WN67.** Service intent is the **union** of the two design radii: if the client is within the
  design radius of either cell, it should receive service. Intent is built from the shared
  fiction's cell intent, using the distance to the nearer cell.
- **WN68.** Handover intent is a separate question: a handover should happen only if it **raises
  the client's rate tier**. A handover that does not change the tier is unnecessary.
- **WN69.** The handover mechanism looks for two conditions: the target cell's signal has to
  exceed the association threshold, **and** exceed the serving cell's signal by the handover
  margin. The handover margin is 3 and stays fixed throughout the measurement.
- **WN70.** An **overlapping** client is one that receives above-threshold signal from both
  cells. A **ping-pong** client is, among overlapping clients, one whose two-signal difference
  falls below the handover margin.
- **WN71.** A client left **in the gap** is one that cannot exceed the threshold from either
  cell.
- **WN72.** The set's resolution is forty clients; the smallest measurable difference is
  **1/40 = 0.025**.

#### Measurement

```python
"""Handover and coverage overlap.

Part 1 - as overlap changes, service's two-directional error.
Part 2 - the handover decision's own two-directional error.
"""
SEED = 20260811
THRESHOLD = 18
TIERS = ((34, 6), (28, 4), (22, 2), (18, 1))
POWER = 58
MARGIN = 3
SPANS = (30, 45, 60, 75, 90)


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

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


def gap(events, intent, mechanism):
    d = {"correct_admit": 0, "correct_deny": 0, "false_admit": 0, "false_deny": 0}
    for e in events:
        n, m = intent(e), mechanism(e)
        if n and m:
            d["correct_admit"] += 1
        elif not n and not m:
            d["correct_deny"] += 1
        elif m:
            d["false_admit"] += 1
        else:
            d["false_deny"] += 1
    return d


def clients(count=40, seed=SEED):
    r, pool = generator(seed), []
    for i in range(count):
        pool.append({"no": i + 1, "distance": 5 + r(41), "interference": r(13)})
    return pool


def cell_intent(c, radius=30):
    return c["distance"] <= radius


def pair(c, span):
    """Distance to and signal from both cells."""
    da, db = c["distance"], abs(span - c["distance"])
    return da, db, POWER - da - c["interference"], POWER - db - c["interference"]


def tier(signal):
    for threshold, k in TIERS:
        if signal >= threshold:
            return k
    return 0


def service_intent(c, span):
    """The design's promise: the union of the two cells' radii is covered."""
    da, db, _, _ = pair(c, span)
    return cell_intent({"distance": min(da, db)})


def service(c, span):
    _, _, ia, ib = pair(c, span)
    return max(ia, ib) >= THRESHOLD


def handover_intent(c, span):
    """A handover should happen only if it raises the rate tier."""
    _, _, ia, ib = pair(c, span)
    return tier(ib) > tier(ia)


def handover(c, span):
    _, _, ia, ib = pair(c, span)
    return ib >= THRESHOLD and ib - ia >= MARGIN


pool = clients()
print(f"clients {len(pool)} | inside cell {sum(cell_intent(c) for c in pool)} | "
      f"served by a single cell "
      f"{sum(1 for c in pool if POWER - c['distance'] - c['interference'] >= THRESHOLD)}")
print()
print("service: union of the two cells")
print(f"{'span':>4s} {'overlapping':>11s} {'ping-pong':>9s} {'in gap':>6s} "
      f"{'served':>6s} {'false admit':>11s} {'false deny':>11s}")
for span in SPANS:
    d = gap(pool, lambda c, s=span: service_intent(c, s),
            lambda c, s=span: service(c, s))
    overlapping = pingpong = in_gap = 0
    for c in pool:
        _, _, ia, ib = pair(c, span)
        if ia >= THRESHOLD and ib >= THRESHOLD:
            overlapping += 1
            if abs(ia - ib) < MARGIN:
                pingpong += 1
        elif ia < THRESHOLD and ib < THRESHOLD:
            in_gap += 1
    print(f"{span:4d} {overlapping:11d} {pingpong:9d} {in_gap:6d} "
          f"{d['correct_admit'] + d['false_admit']:6d} "
          f"{d['false_admit']:11d} {d['false_deny']:11d}")
print()
print("handover: must raise the tier")
print(f"{'span':>4s} {'intended handover':>17s} {'handover made':>13s} "
      f"{'false admit':>11s} {'false deny':>11s}")
for span in SPANS:
    d = gap(pool, lambda c, s=span: handover_intent(c, s),
            lambda c, s=span: handover(c, s))
    print(f"{span:4d} {sum(handover_intent(c, span) for c in pool):17d} "
          f"{d['correct_admit'] + d['false_admit']:13d} "
          f"{d['false_admit']:11d} {d['false_deny']:11d}")
```

```
clients 40 | inside cell 27 | served by a single cell 31

service: union of the two cells
span overlapping ping-pong in gap served false admit  false deny
  30          31         1      0     40           0           0
  45          28         3      0     40           0           0
  60          11         4      1     39           0           1
  75           1         0      7     33           7           1
  90           0         0      9     31           5           1

handover: must raise the tier
span intended handover handover made false admit  false deny
  30                27            30           3           0
  45                19            21           2           0
  60                13            12           0           1
  75                 2             2           0           0
  90                 0             0           0           0
```

#### As Overlap Narrows

The bottom row is a check point. At span 90, the second cell reaches no one: overlapping is 0,
handover is 0, and the service table reproduces the shared fiction's single-cell power-58 row
exactly — **31** served, **5** false admit, **1** false deny. The two-cell measurement collapses
into the single-cell measurement once the cells are far enough apart.

As the span narrows, both of service's errors fall: at 60 they are 0 and 1; at 45 and 30 both are
0. All forty of the forty clients receive service, and none is outside intent. On paper, this is
a flawless configuration — and this is exactly where the lesson's warning sits.

The cost sits in the `overlapping` column: at span 30, 31 clients receive above-threshold signal
from both cells at once. Thirty-one of the forty clients stand on ground both cells cover; the
second cell is not extending coverage, it is covering the same ground a second time. **The
configuration that zeroes out the error columns is the configuration that re-spends half of
coverage.** This cost does not show up in the two directions the `gap` function counts; it shows
up in a different column.

The span 75 row carries the warning in the other direction. False admit rises to **7** here —
higher than the single cell's 5. As the two cells pull apart, each leaks from its own edge, and
the leaks add up; **adding a cell does not divide the leak, it multiplies it.** In the same row, 7
clients are left in the gap: neither cell exceeds the threshold for them. The same configuration
both leaks extra and leaves a gap.

The `ping-pong` column behaves unexpectedly: **1, 3, 4, 0, 0**. The widest overlap has the fewest
unstable clients. The reason is this: instability does not depend on the size of the overlap but
on **client density around the two cells' midline**. At span 30 the midline sits at 15 meters, and
few clients sit there; at span 60 the midline shifts to 30 meters, and that is where the crowd
happens to fall. Instability is not a coverage measure, it is a **placement** measure.

#### Handover's Own Two Errors

The second table measures the handover decision against its own intent: a handover is correct
only if it raises the rate tier.

At span 30, intent wants 27 handovers, the mechanism makes 30, and 3 are **unnecessary** —
handovers that do not change the tier. At span 45 the same direction continues: 2 unnecessary
handovers. When overlap is generous, the handover margin gets exceeded for a large number of
clients, but the margin it exceeds by is not enough to raise the tier. This is an operation
wasted on the control plane: report, context transfer, and command, with no gain at all.

At span 60 the sign flips: unnecessary handovers is 0, **missed handovers** is 1. As overlap
narrows, fewer clients exceed the handover margin, and the margin can end up blocking a handover
that would have raised the tier. The same fixed margin produces false admit under generous
overlap and false deny under narrow overlap. The error's direction changed without the margin
changing at all; the only thing that changed was the distance between the cells.

Placing the two tables side by side gives the lesson's result. The configurations that look best
in the service table — spans 30 and 45 — are the configurations that produce the most unnecessary
handovers in the handover table. An audit that looks at only one table gives opposite answers
depending on which one it looks at.

#### Summary

- Cellular architecture splits into a radio access side and a core side; handover is a
  control-plane job, and the data plane is carried from the old cell to the new one while it
  happens.
- The network makes the decision, not the client: the client sends the measurement report, the
  serving cell agrees on context with the target and sends the handover command; this gathers
  the decision in a single hand and brings a round-trip debt.
- As inter-site distance is pulled in from 90 to 30, service's two errors fall from **5/1** to
  **0/0**, but overlapping clients rise from **0** to **31**; zero error is paid for by spending
  coverage a second time.
- At span 75, false admit becomes **7** — higher than the single cell's 5 — and in the same row
  7 clients are left in the gap; adding a cell does not divide the leak, it multiplies it.
- A fixed handover margin produces **3** unnecessary handovers under generous overlap and **1**
  missed handover under narrow overlap; what decides the error's direction is not the margin's
  value but the distance between the cells.

#### Next Step

Throughout this course's wireless half, what drew the boundary was always the same kind of thing:
distance, interference, power budget, generation ceiling, cell placement. All of them are
properties of the **medium**. No one sat down and wrote a rule saying "let this one through, stop
that one"; the boundary emerged as a byproduct of physics and placement, and it departed from
intent in both directions at once.

The next topic takes the boundary out of the medium's hands. Once the boundary is **written** —
once who gets through and who is stopped is declared explicitly, as an ordered list — does the
deviation from intent disappear, or does it only change location? The next lesson opens this
question, and the two directions it counts stay the same: false admit and false deny.

## Network Security

Firewall types, rule writing and evaluation order, trust zones and the demilitarized zone, intrusion detection and prevention, tunneling technologies, the zero trust model, and the classes defense does not cover.

### Firewall Types

Source: https://academia.sh/en/courses/wireless-and-security/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.

### Access Control Lists

Source: https://academia.sh/en/courses/wireless-and-security/access-control-lists

Four rule sets on the same forty flows: the seven-rule narrow set gives 40/40 correct, the six-rule wide set false-admits 2 flows, the eight-rule misordered set also admits 2 and shadows 1 rule, and the seven-rule tightened set zeroes the leak while false-denying 2 flows.

The previous lesson held the chain fixed and changed the mechanism running it. With seven
rules, the stateful mechanism gave the same decision as intent on all forty flows — but we
never showed that those seven rules were the right seven. It was taken on faith.

This lesson does the reverse: the mechanism is fixed, and what changes is the chain
itself. An **access control list** is an ordered sequence of (action, criteria) pairs; the
criteria requires all the fields it writes to hold at once, and a field it does not write
matches any value. Its question is this: can we tell, just by looking at a rule set, how
well it meets intent?

#### Two Things That Give the Chain Meaning

**Evaluation order** is as decisive as the criteria itself. The flow enters the chain from
the top, the first matching rule's action is applied, and the chain ends there. Arranging
the same rules in a different order draws a different boundary.

**Implicit deny** stops at wherever the chain ends and is never written. Every rule in all
four sets in this lesson has the **permit** action; the only thing that blocks is the
implicit deny. This has a measurement consequence: every blocked flow is blocked from a
single place, a place with no name.

The Perimeter Defense topic of the Cybersecurity curriculum taught the **syntax** of
access control lists — which field goes where, how a list is built. That syntax is not
repeated here. What is measured is not the list's syntax but **the semantics of an ordered
chain itself**: where the chain departs from intent in both directions at once.

Three more sets join the previous lesson's seven-rule **narrow** set.

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

wide (6) : the narrow set's first two rules collapsed into one
  1  permit  ext  dmz  *          <- 80 and 443, instead of separately
  2..6 = narrow set's rules 3..7

misordered (8) : a rule added to the front of the narrow set
  1  permit  dmz  *    *          <- new , at the very front of the chain
  2..8 = narrow set's rules 1..7

tightened (7) : the narrow set's rule 6 narrowed
  6  permit  int  ext  443        <- port 443 only, instead of every port
```

#### Rule Shadowing

The change in the third set makes one rule **unreachable**. When a rule that permits every
flow sourced from `dmz` is placed at the front of the chain, no flow can reach the "only
`dmz` to `int`, port 3306" rule below it — because every flow that could reach it has
already matched above. This is called **rule shadowing**.

The definition is **static**: a rule is shadowed if it is fully contained by an earlier
rule's criteria. This can be said without looking at traffic at all, just by looking at
the chain. Why the distinction matters will be seen after the measurement.

The measurement's assumptions:

- **NS8** — The forty flows are taken unchanged from the shared fixture; their source,
  destination, and port come from the fixture. The oracle is intent itself, known because
  we wrote the fixture.
- **NS9** — All four sets run on the same mechanism: first match wins, the chain ends in
  implicit deny, every flow counts as a new connection. The previous lesson's phase
  distinction is absent here.
- **NS10** — A criterion a rule does not write matches any value; the criteria it does
  write must all hold at once.
- **NS11** — All four sets' rules carry the **permit** action. The only thing that blocks
  is the implicit deny; this concentrates the source of every false deny in one place.
- **NS12** — Rule shadowing is defined statically and does not look at traffic; the match
  counter looks only at traffic. The two are measured separately.
- **NS13** — The match counter counts where the chain **cuts off**: a flow stops at the
  first rule it matches and never enters the rules after it.
- **NS14** — In a set of forty events, the smallest measurable difference is
  **1/40 = 0.025**.

#### Measurement

```python
"""Access control lists: rule count, evaluation order, shadowing.

Part 1 - four sets on the same forty flows.
Part 2 - which rule matched how many flows, how many fell to implicit deny.
"""
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 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


def shadowed_rules(rules):
    """The index of every rule fully covered by a rule ahead of it."""
    out = []
    for i, (_, criteria) in enumerate(rules):
        for _, earlier in rules[:i]:
            if all(criteria.get(k) == v for k, v in earlier.items()):
                out.append(i)
                break
    return out


def shadowed_count(rules):
    return len(shadowed_rules(rules))


def trace(rules, ak):
    """How many flows each rule matched, and how many fell to implicit deny."""
    counts, denied = [0] * len(rules), 0
    for a in ak:
        for i, (_, criteria) in enumerate(rules):
            if all(a[k] == v for k, v in criteria.items()):
                counts[i] += 1
                break
        else:
            denied += 1
    return counts, denied


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"})]

WIDE = [(True, {"source": "ext", "dest": "dmz"})] + NARROW[2:]
MISORDERED = [(True, {"source": "dmz"})] + NARROW
TIGHTENED = NARROW[:5] + [(True, {"source": "int", "dest": "ext", "port": 443})] + NARROW[6:]

SETS = (("narrow", NARROW), ("wide", WIDE),
        ("misordered", MISORDERED), ("tightened", TIGHTENED))
AK = flows()

print(f"flow {len(AK)} | intent passes {sum(1 for a in AK if policy(a))} "
      f"| intent blocks {sum(1 for a in AK if not policy(a))}")
print()
print(f"{'rule set':<14s} {'rules':>5s} {'shadowed':>8s} {'implicit deny':>13s} "
      f"{'correct':>7s} {'false admit':>11s} {'false deny':>11s}")
for name, k in SETS:
    d = diff(AK, policy, rule_chain(k))
    _, denied = trace(k, AK)
    print(f"{name:<14s} {len(k):5d} {shadowed_count(k):8d} {denied:13d} "
          f"{d['correct_pass'] + d['correct_block']:7d} {d['false_admit']:11d} "
          f"{d['false_deny']:11d}")

print()
print("narrow set, matches per rule:", trace(NARROW, AK)[0])
print("misordered set, matches per rule:")
counts, denied = trace(MISORDERED, AK)
shadow = shadowed_rules(MISORDERED)
for i, ((_, criteria), n) in enumerate(zip(MISORDERED, counts)):
    print(f"  {i + 1}. {str(criteria):<48s} {n:3d}"
          f"  {'shadowed' if i in shadow else ''}".rstrip())
print(f"  implicit deny{'':40s} {denied:3d}")

print()
for name, k in SETS[1:]:
    d = rule_chain(k)
    print(f"{name}: false admit",
          [(a["no"], a["source"], a["dest"], a["port"])
           for a in AK if d(a) and not policy(a)],
          "| false deny",
          [(a["no"], a["source"], a["dest"], a["port"])
           for a in AK if policy(a) and not d(a)])
```

```
flow 40 | intent passes 22 | intent blocks 18

rule set       rules shadowed implicit deny correct false admit  false deny
narrow             7        0            18      40           0           0
wide               6        0            16      38           2           0
misordered         8        1            16      38           2           0
tightened          7        0            20      38           0           2

narrow set, matches per rule: [0, 1, 0, 11, 4, 3, 3]
misordered set, matches per rule:
  1. {'source': 'dmz'}                                  2
  2. {'source': 'ext', 'dest': 'dmz', 'port': 80}       0
  3. {'source': 'ext', 'dest': 'dmz', 'port': 443}      1
  4. {'source': 'dmz', 'dest': 'int', 'port': 3306}     0  shadowed
  5. {'source': 'mgmt'}                                11
  6. {'source': 'int', 'dest': 'dmz'}                   4
  7. {'source': 'int', 'dest': 'ext'}                   3
  8. {'source': 'int', 'dest': 'int'}                   3
  implicit deny                                          16

wide: false admit [(3, 'ext', 'dmz', 3306), (18, 'ext', 'dmz', 8080)] | false deny []
misordered: false admit [(23, 'dmz', 'mgmt', 8080), (39, 'dmz', 'mgmt', 8080)] | false deny []
tightened: false admit [] | false deny [(30, 'int', 'ext', 3306), (35, 'int', 'ext', 3306)]
```

#### Counting Rules Does Not Measure Policy

Reading the four rows by rule count gives the order 6, 7, 7, 8. Reading them by correct
count gives 38, 40, 38, 38. There is no link between the two orderings.

**Seven rules were written twice and gave two different results.** The narrow set is
40/40 correct; the tightened set, with the same rule count, gets 38. **Eight rules are not
better than seven:** the misordered set is one rule longer and stayed at 38. **Six rules
are not simpler than seven:** the wide set is one rule shorter and is also 38. A set's
length says nothing about its deviation from intent.

The **direction** of the deviation cannot be read from the set either. All three flawed
sets give 38 correct, but:

**Wide set: 2 false admit, 0 false deny.** A rule's port field was deleted, and two flows
going from `ext` to `dmz` — one on 3306, one on 8080 — passed although intent would have
blocked them. Rule count dropped by one; the open surface grew by two flows.

**Misordered set: 2 false admit, 0 false deny.** The rule added to the front of the chain
permits everything sourced from `dmz`; two flows from `dmz` to the management zone leaked
through this way. Yet this was intent's most definite statement: nobody from outside to
the management zone.

**Tightened set: 0 false admit, 2 false deny.** No leak here at all — flawless on paper.
The cost shows up in the other column: two flows going from `int` to `ext` were cut
because their port was not 443, and intent would have passed both. An audit that only
counts leaks cannot tell this set apart from the narrow set.

Two flows each are a share of **0.050** in a set of forty; since the smallest measurable
difference is **0.025**, both are within the measurement band.

#### Zero Matches Says Two Different Things

The lower table shows the chain's operation rule by rule. Two rules in the misordered set
match **0** flows: the second rule (`ext dmz 80`) and the fourth rule (`dmz int 3306`).
The counter writes the same thing for both. Their reasons are not the same.

The second rule is reachable; none of the forty flows happens to match it. It would match
if the traffic changed. The fourth rule, however, is **unreachable**: every flow that
could reach it is cut off at the first rule. It will not match no matter what the traffic
is. The counter cannot tell these two apart; **static scanning** can, and it flags only
the fourth one in the last column. In the narrow set, too, the third rule matches 0 flows,
and there is no shadowed rule there — the same observation, a different reason.

Had the shadowed rule's action been **different** from the one shadowing it, the result
would not have been this quiet. All four sets in this lesson had every rule carry the
permit action (NS11). In a mixed chain, a **deny** rule written underneath a wide
**permit** rule is never applied even though it was written; the chain keeps open a path
its author thought was closed. Static scanning flags both cases the same way — comparing
actions is not the scan's job — but the weight of the flag is not the same: in one case
only readability suffers, in the other, policy itself breaks.

The shadowed rule itself does no harm in this measurement. It carries the same action as
the rule shadowing it, so deleting it changes no decision across the forty flows. The rule
doing the harm is the shadowing one, and its cost is paid elsewhere: in the two flows
leaking into the management zone. The shadowed rule is a **symptom**, not the leak itself.
What the symptom says is this: someone reading the chain sees the line "`dmz` to `int`,
only the data entry is open" and believes it, when two lines above, that zone has already
been opened entirely. The rule chain does not run the way it reads.

#### Surfaces Left Open, and Narrowings

**First surface — a deleted criteria field.** In the wide set, deleting the port field
saved one rule and leaked two flows. **Narrowing:** write the criterion. The cost is
exactly one rule, and in exchange false admit drops from **2** to **0**. Simplification
achieved by deleting a criteria field looks like an improvement to an audit that reads
rule count.

**Second surface — a wide rule placed at the front.** When a rule opening an entire zone
is placed at the very front, it neutralizes every narrowing rule that follows it.
**Narrowing:** place rules by **criteria scope**, not by insertion order — narrowly-scoped
rules first, wide ones later — and run the chain through static scanning after every
change. The scan catches the shadowed rule in this set and points at the wide rule.

**Third surface — the silence of implicit deny.** In the tightened set, implicit deny cuts
**20** flows; **18** of them are flows intent also cuts, **2** are false denies. Both are
cut from the same place, a place with no name, and neither has a rule name standing
against it. **Narrowing:** write a rule at the end of the chain whose action is **deny**
and that leaves every criterion open. It changes no decision — implicit deny was already
doing the same thing — but it makes the blocked flow countable. False deny can only be
measured where it can be counted.

#### Summary

- An access control list's meaning does not come from its criteria alone, but from the
  chain the criteria and the evaluation order build together; the chain ends in an
  unwritten implicit deny.
- On the same forty flows, the narrow set gives 40/40 correct with **7** rules; the wide
  set produces **2** false admits with **6** rules; the misordered set produces **2**
  false admits and **1** shadowed rule with **8** rules; the tightened set produces **2**
  false denies with **7** rules.
- There is no link between rule count and deviation from intent: seven rules were written
  twice, one gave 40 correct, the other 38. **Counting rules does not measure policy.**
- The direction of the deviation cannot be read from the set either; all three flawed sets
  give 38 correct, but two of them false-admit and one false-denies.
- Zero matches comes from two different causes — an unused rule and an unreachable rule —
  and the counter cannot tell them apart; rule shadowing is visible only through static
  scanning.
- A shadowed rule is a symptom; what produces the leak is the wide rule shadowing it.

#### Next Step

Every rule in this lesson was written over four names: `ext`, `dmz`, `int`, `mgmt`. These
names came from the fixture and were never questioned — as if the network's zones were
just given. But the number of zones is a design decision: the same network can be split
into two zones, or six. The next lesson derives rules from a fixed intent and changes the
zone graph, measuring two things at once: how many rules are needed as the zone count
grows, and which direction merging or splitting zones shifts the deviation from intent.

### Network Segmentation and DMZ

Source: https://academia.sh/en/courses/wireless-and-security/network-segmentation-and-dmz

The same intent is turned into a rule set across four zone graphs: in two zones, the narrowest derivation that never leaks passes nothing and false-denies 22 flows, while the wide derivation leaks 6; in four zones, 38 rules zero the deviation, and in six zones the rule count climbs to 88 while the deviation stays at zero.

Every rule in the previous lesson was written over four names: `ext`, `dmz`, `int`,
`mgmt`. These names came from the fixture and were never questioned — as if the network's
zones were just given. Yet no rule names a machine; every one names a **zone**. Take the
zones away, and there is no rule left that can be written.

This lesson's question is: who chooses the zones, how many, and how much of intent does
that choice make sayable? In this lesson the rule set is not written by hand; it is
**derived from the zone graph**. This way the only thing that changes is the graph.

#### A Zone Is a Policy Object

The **segmentation** mechanism is not this course's subject: how a broadcast domain is
split, how virtual LANs are tagged, and how a switch enforces this were established in the
Switching and Routing course. The zone here is not that mechanism's product; it is **the
smallest name policy can speak in.**

The difference shows up here: as long as two machines are in the same zone, no rule can be
written between them. The zone is the rule's resolution. To be able to say an intent, the
graph must distinguish everything the intent distinguishes.

The **demilitarized zone (DMZ)** exists exactly for this reason. It is intent's middle
term: a place open to the outside but not belonging to the inside. Without this name, only
two options remain for a flow coming from outside — either it is let into the inside, or
it is not let in at all.

```text
# taught zone graph , not executed
# the names are fictional

  ext ──80,443──> dmz ──3306──> int <──> int
                                 │
   x─────────────────────────────┴───x─── mgmt
                          (mgmt goes everywhere , nobody goes to it)

merging  :  2 zones  ext | dmz+int+mgmt
            3 zones  ext | dmz | int+mgmt
splitting:  6 zones  ext | dmz-web | dmz-pub | int-data | int-app | mgmt
```

The Perimeter Defense topic of the Cybersecurity curriculum covered the **design** of
trust zones: which asset goes in which zone, how layers are arranged. That design
narrative is not repeated here. What is measured is **how much of policy the zone graph
can meet.**

#### The Graph Derives the Rules

Given a graph, one or more **base** pairs collect under every zone pair and every port. As
the graph gets coarser, more than one base pair accumulates under a single pair, and a
single decision has to be made for that pair. Two derivations are possible:

The **narrow derivation** writes the rule only if **all** the base pairs underneath it are
permitted. It never leaks; it also cuts the permitted ones among mixed pairs.

The **wide derivation** writes the rule if even **one** of the pairs underneath it is
permitted. It never cuts a permitted flow; it also passes the forbidden ones among mixed
pairs.

If the graph can distinguish all the base zones, no pair has any mixture underneath it,
and the two derivations give the same set. **Ambiguity is not born from the graph; it is
born from merging.**

One warning: this derivation writes the set in **expanded** form, producing a separate
rule for every port. The previous lesson's seven-rule set wrote the same boundary in
**compressed** form — an unwritten field matched any value. The two counts do not belong
on the same scale; what this lesson compares is the **ratio** across graphs.

The measurement's assumptions:

- **NS15** — The forty flows are taken unchanged from the shared fixture; the oracle is
  intent itself, and in every measurement it looks at base zone names.
- **NS16** — For the fine six-zone graph, each flow's two ends are placed into one of
  their base zone's subzones by a choice from a separate generator. An end's base zone
  does not change; it only gets a finer name.
- **NS17** — The rule set is not written by hand; it is derived from the graph. The only
  thing that changes is the graph; policy and intent are fixed.
- **NS18** — The derivation writes rules in expanded form: a separate rule for every zone
  pair and every port. These rule counts are not compared with the previous lesson's
  compressed sets.
- **NS19** — Every derived rule carries the **permit** action; the only thing that blocks
  is the implicit deny.
- **NS20** — The graph changes only the zone names. The port set, the flow count, and
  intent are the same across all four graphs.
- **NS21** — In a set of forty events, the smallest measurable difference is
  **1/40 = 0.025**.

#### Measurement

```python
"""Network segmentation: rule count and two-way error as the zone graph changes.

Part 1 - the graph derives a rule set from the same intent.
Part 2 - the effect of merging and splitting on false admit / false deny.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
PORTS = (80, 443, 22, 3306, 8080)
FINE = ("ext", "dmz-web", "dmz-pub", "int-data", "int-app", "mgmt")
BASE = {"ext": "ext", "dmz-web": "dmz", "dmz-pub": "dmz",
        "int-data": "int", "int-app": "int", "mgmt": "mgmt"}


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 subzone(ak, seed=SEED + 3):
    """Places each end into one of its base zone's subzones."""
    r = generator(seed)
    pairs = {"dmz": ("dmz-web", "dmz-pub", "dmz-web", "dmz-pub"),
             "int": ("int-data", "int-app", "int-app", "int-data")}
    for a in ak:
        for end in ("source", "dest"):
            base = a[end]
            a["fine_" + end] = pairs[base][r(4)] if base in pairs else base
    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


GRAPHS = (
    ("2 zones", {f: ("ext" if f == "ext" else "int") for f in FINE}),
    ("3 zones", {f: ("int" if f == "mgmt" else BASE[f]) for f in FINE}),
    ("4 zones", dict(BASE)),
    ("6 zones", {f: f for f in FINE}),
)


def build_rules(graph, loose):
    """The rule set that meets intent as far as the graph can tell things apart."""
    names = sorted(set(graph.values()))
    rules = []
    for z1 in names:
        for z2 in names:
            for port in PORTS:
                allowed = [policy({"source": BASE[f1], "dest": BASE[f2],
                                   "port": port})
                           for f1 in FINE if graph[f1] == z1
                           for f2 in FINE if graph[f2] == z2]
                if all(allowed) or (loose and any(allowed)):
                    rules.append((True, {"source": z1, "dest": z2,
                                         "port": port}))
    return rules


def device(graph, rules):
    k = rule_chain(rules)
    return lambda a: k({"source": graph[a["fine_source"]],
                        "dest": graph[a["fine_dest"]],
                        "port": a["port"]})


AK = subzone(flows())
print(f"flow {len(AK)} | intent passes {sum(1 for a in AK if policy(a))} "
      f"| fine zones {len(FINE)}")
print()
print(f"{'zone graph':<12s} {'zones':>5s} {'derivation':<10s} {'rules':>5s} "
      f"{'correct':>7s} {'false admit':>11s} {'false deny':>11s}")
for name, g in GRAPHS:
    for kind, loose in (("narrow", False), ("wide", True)):
        k = build_rules(g, loose)
        d = diff(AK, policy, device(g, k))
        print(f"{name:<12s} {len(set(g.values())):5d} {kind:<10s} {len(k):5d} "
              f"{d['correct_pass'] + d['correct_block']:7d} {d['false_admit']:11d} "
              f"{d['false_deny']:11d}")

print()
for name, g in GRAPHS[:2]:
    f = device(g, build_rules(g, True))
    print(f"{name} wide derivation's false admits:",
          [(a["no"], a["source"], a["dest"], a["port"])
           for a in AK if not policy(a) and f(a)])

FINE_GRAPH = GRAPHS[3][1]
MISSING = [(e, o) for e, o in build_rules(FINE_GRAPH, False) if o["dest"] != "int-app"]
INCOMPLETE = device(FINE_GRAPH, MISSING)
d = diff(AK, policy, INCOMPLETE)
print()
print(f"6 zones, rules targeting 'int-app' left unwritten: rules {len(MISSING)}, "
      f"correct {d['correct_pass'] + d['correct_block']}, "
      f"false admit {d['false_admit']}, false deny {d['false_deny']}")
print("flows cut:",
      [(a["no"], a["fine_source"], a["fine_dest"], a["port"])
       for a in AK if policy(a) and not INCOMPLETE(a)])
```

```
flow 40 | intent passes 22 | fine zones 6

zone graph   zones derivation rules correct false admit  false deny
2 zones          2 narrow         0      18           0          22
2 zones          2 wide          12      34           6           0
3 zones          3 narrow        12      29           0          11
3 zones          3 wide          18      38           2           0
4 zones          4 narrow        38      40           0           0
4 zones          4 wide          38      40           0           0
6 zones          6 narrow        88      40           0           0
6 zones          6 wide          88      40           0           0

2 zones wide derivation's false admits: [(23, 'dmz', 'mgmt', 8080), (26, 'int', 'mgmt', 8080), (27, 'int', 'mgmt', 8080), (32, 'ext', 'int', 443), (36, 'ext', 'int', 443), (39, 'dmz', 'mgmt', 8080)]
3 zones wide derivation's false admits: [(26, 'int', 'mgmt', 8080), (27, 'int', 'mgmt', 8080)]

6 zones, rules targeting 'int-app' left unwritten: rules 71, correct 38, false admit 0, false deny 2
flows cut: [(4, 'int-data', 'int-app', 443), (38, 'mgmt', 'int-app', 443)]
```

#### What Merging Costs

The two-zone graph's narrow derivation produces **zero rules**. This is not a writing
error; it is the answer the derivation gives: in a two-zone space, no pair is permitted
across all of its underlying base pairs. The entire boundary falls to the implicit deny.
False admit **0** — flawless on paper. False deny **22**: all twenty-two flows intent
would pass are cut.

The wide derivation of the same graph writes **12** rules and flips the direction of the
error: false deny **0**, false admit **6**. Four of the six leaking flows go to the
management zone, two go straight from outside to the inside. The graph did not change,
intent did not change; the only thing that changed was how a mixed pair gets read. **When
a graph collapses to two zones, the direction of the error is no longer the design's
choice but the derivation's.** This is the DMZ's rationale in one line: without that name,
the sentence "open to the outside but not belonging to the inside" cannot be said.

The three-zone graph separates the outside and the DMZ, leaving management inside. The
narrow derivation false-denies **11** flows with **12** rules; the wide derivation, with
**18** rules, leaks only **2** flows, and both go from `int` to management. Putting
management under the same name as the inside makes the sentence "nobody goes from the
inside to management" unsayable — one zone merge directly erases a prohibition.

In the four-zone graph, the two derivations give **the same 38 rules**, and the deviation
is zero in both directions. This is the expected outcome: the graph distinguishes
everything intent distinguishes, no mixed pair remains, there is nothing left to choose
between.

#### What Splitting Does Not Pay For

The six-zone graph splits the DMZ and the inside into two pieces each. The zone count
rises from 4 to 6 — **1.5 times**. The rule count rises from 38 to 88 — **2.3 times**; as
long as the derivation writes a rule for every zone pair, this growth tracks the square of
the zone count. The deviation from intent, meanwhile, goes from **0** to **0**. The
decision was already correct on all forty flows, and it stays correct.

What the measure says is this: **adding zones does not move you closer to intent.** Four
zones could already say intent; six zones say it at greater length. This does not mean
splitting is useless — it opens up **other** intents that can be said. But nothing is
gained on this measure, and the cost is fifty rules.

Splitting carries a risk of its own, and it shows up in the lower table. When a zone is
split in two, rules must be rewritten separately for both halves; when one half is
forgotten, the set drops to **71** rules, false admit stays at **0**, and false deny
climbs to **2**. Both flows go to the forgotten half of the split. The splitting error is
one-directional: because the new zone name appears in no rule, it opens nothing — it only
closes.

#### Surfaces Left Open, and Narrowings

**First surface — the wide derivation's mixed pairs.** Six flows in the two-zone graph,
two flows in the three-zone graph, pass although intent forbids them. These are not a
configuration mistake; they are things the graph **cannot say**. **Narrowing:** a new zone
name. Moving management to its own name zeroes the two leaks in the three-zone graph;
separating out the DMZ closes four of the six in the two-zone graph. What gets added is
not a rule but a **name**.

**Second surface — the narrow derivation's silent cutting.** 22 flows in the two-zone
graph, 11 in the three-zone graph, fall to the implicit deny, and none of them has a rule
name standing against it. An audit that only counts leaks would declare the two-zone graph
flawless. **Narrowing:** build the measurement two-directionally, and write a countable
deny rule at the end of the chain — the previous lesson's narrowing applies here too.

**Third surface — the forgotten half of a split zone.** Splitting a zone in two doubles
every rule that touches it. In the measurement, the forgotten half is visible because it
only produces false denies — the user complains. The reverse direction is quieter: if the
new name falls under a wide rule that was never meant to touch it, it goes entirely
unnoticed. **Narrowing:** never separate the split from the rule-writing; rerun the
derivation from scratch for every new zone name, and compute the expected growth in rule
count ahead of time. Fewer rules than expected means an unwritten rule.

#### Summary

- A zone is not a segmentation mechanism; it is **the smallest name policy can speak in**.
  If two machines are in the same zone, no rule can be written between them.
- In a coarse graph, mixed base pairs accumulate under every zone pair, giving rise to two
  derivations: the narrow derivation never leaks but false-denies, the wide derivation
  never false-denies but leaks.
- In the two-zone graph, the narrow derivation writes **0** rules and false-denies **22**
  flows; the wide derivation leaks **6** flows with **12** rules. This is the DMZ's
  rationale — without that name, intent's middle term cannot be said.
- In four zones, the two derivations give the same **38** rules and the deviation is zero
  in both directions; ambiguity is born from merging, not from the graph.
- As zones rise from 4 to 6, rules rise from 38 to 88 while the deviation stays at **0**:
  adding zones does not move you closer to intent, it only says the same intent at
  greater length.
- Splitting a zone and leaving one half's rules unwritten produces **0** false admits and
  **2** false denies; the splitting error is one-directional.

#### Next Step

The three lessons so far each treated the boundary as a mechanism that decides **once**: a
flow arrives, runs through the rule chain, passes or blocks. The decision is read from the
flow's own fields and ends there. But whether a flow matches intent is sometimes not told
by its fields but by its **behavior** — and behavior is read through a threshold. The next
lesson sets the rule chain aside and measures the threshold: as the threshold tightens, the
number of missed flows falls, the number of pointlessly blocked flows rises, and the two
numbers cannot be zeroed at the same time. The same measure gives the same number on a
detecting mechanism and a preventing one, but it does not charge the same cost.

### Intrusion Detection and Prevention

Source: https://academia.sh/en/courses/wireless-and-security/intrusion-detection-and-prevention

In a threshold-based mechanism, the two errors are tied to each other: in the network-based system, at threshold 7 false admit is 0 but false deny is 7; at threshold 9 false deny is 0 but false admit is 5. The host-based system's 8 forbidden flows, meanwhile, never show up at any threshold, because it never sees them.

The previous three lessons each treated the boundary as a mechanism that decides **once**.
A flow arrives, its fields are read, it runs through the rule chain, it passes or blocks.
The decision is exact because the criterion is exact: a flow either is sourced from `dmz`
or it is not, there is nothing in between.

This lesson looks at a criterion that is not exact. An **intrusion detection system** can
evaluate a flow in two different ways: the kind that looks for a known pattern does the same
field matching as the previous three lessons; the kind this lesson looks at evaluates a flow
by its **behavior**, and reduces that behavior to a number. That number is compared against
a **threshold**. What is new is not the threshold
itself but the number's **overlap** between two classes — and this entire lesson takes
place inside that overlap.

#### A Threshold Is Not a Rule

The rule chain's criterion is binary, and it distinguishes what it distinguishes
flawlessly. The threshold's criterion is continuous, and the values of the two classes it
distinguishes interleave. The numbers of flows intent passes are low, of those it blocks
are high; but the two distributions stack on top of each other in a band, and inside that
band no threshold gives the right answer.

```text
# taught scale , not executed
# the axis is the score , the numbers come from the measurement block

  flows intent passes    |==========|
  flows intent blocks           |==============|
                                ^^^^  overlap band

  the threshold sits somewhere on this axis :
    shift left  -> false deny grows , false admit shrinks
    shift right -> false admit grows , false deny shrinks
    it cannot step outside the band
```

This is not a flaw in the mechanism; it is the nature of the criterion. If the criterion
distinguished flawlessly, a rule could already be written and there would be no need for a
threshold.

#### What the Two Systems See

The **network-based** system evaluates the flow itself and sees all forty flows regardless
of zone. What it sees is the outside of the flow; its number is noisy.

The **host-based** system runs on the endpoint the flow lands on. Because it sees the
context at the endpoint, its number is sharper, but it sees only the flow **landing on its
own host**; a flow landing on another zone never enters its field, and it has no decision
about that flow.

The Perimeter Defense topic of the Cybersecurity curriculum covered these systems by their
**placement** and their **signature procedure**: where the sensor goes, how a signature is
written, how a given record gets matched. That narrative is not repeated here. What is
measured is not placement but **how the two-way error behaves as a function of the
threshold**.

The measurement's assumptions:

- **NS22** — The forty flows are taken unchanged from the shared fixture; the oracle is
  intent itself, and it says whether a flow really matches intent.
- **NS23** — The score is derived from the oracle: a flow intent blocks starts from a base
  of 7, one intent passes from a base of 2, and noise from a separate generator is added
  on top. This is the fixture because what a detection system sees is the violation's
  **noisy trace**; the overlap band is known because we wrote the trace.
- **NS24** — The network-based system's noise width is 7, the host-based system's is 6.
  The host-based system's sharper reading comes from this width difference; the two
  scores come from separate generators and are independent of each other.
- **NS25** — The host-based system sees only flows whose destination is `int` or `mgmt`;
  for other flows its decision is **pass**, because it has no decision.
- **NS26** — The mechanism's decision has the form `score < threshold`; as the threshold
  rises, fewer flows are blocked.
- **NS27** — When the two run together, a flow passes only if **both** pass it.
- **NS28** — The `blocked` column counts every flow the mechanism blocks, right and wrong
  alike. In detection mode this column is the **alert** count; in prevention mode it is
  the **cut flow** count.
- **NS29** — In a set of forty events, the smallest measurable difference is
  **1/40 = 0.025**.

#### Measurement

```python
"""Intrusion detection and prevention: how the two-way error shifts as the threshold moves.

Part 1 - network-based mechanism, threshold sweep.
Part 2 - host-based mechanism, and the two mechanisms combined.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
PORTS = (80, 443, 22, 3306, 8080)
HOST_SEEN = ("int", "mgmt")


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 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 score(ak, field, width, seed):
    """The violation's noisy trace: a forbidden flow starts at 7, a permitted one at 2."""
    r = generator(seed)
    for a in ak:
        a[field] = (7 if not policy(a) else 2) + r(width)
    return ak


def network_based(threshold):
    return lambda a: a["net_score"] < threshold


def host_based(threshold):
    """Sees only the flow landing on its own host; passes everything else."""
    return lambda a: a["host_score"] < threshold if a["dest"] in HOST_SEEN else True


def combined(th_net, th_host):
    return lambda a: network_based(th_net)(a) and host_based(th_host)(a)


AK = score(score(flows(), "net_score", 7, SEED + 5),
           "host_score", 6, SEED + 7)

print(f"flow {len(AK)} | intent passes {sum(1 for a in AK if policy(a))} "
      f"| seen by the host-based mechanism "
      f"{sum(1 for a in AK if a['dest'] in HOST_SEEN)}")
print(f"network score: permitted {min(a['net_score'] for a in AK if policy(a))}"
      f"-{max(a['net_score'] for a in AK if policy(a))}, "
      f"forbidden {min(a['net_score'] for a in AK if not policy(a))}"
      f"-{max(a['net_score'] for a in AK if not policy(a))}")
print()
print(f"{'mechanism':<16s} {'threshold':>9s} {'correct':>7s} {'false admit':>11s} "
      f"{'false deny':>11s} {'blocked':>7s}")
trials = ([("network-based", e, network_based(e)) for e in range(6, 13)]
          + [("host-based", e, host_based(e)) for e in range(6, 10)]
          + [("combined", e, combined(e, e)) for e in (8, 9)])
for name, e, f in trials:
    d = diff(AK, policy, f)
    print(f"{name:<16s} {e:9d} {d['correct_pass'] + d['correct_block']:7d} "
          f"{d['false_admit']:11d} {d['false_deny']:11d} "
          f"{sum(1 for a in AK if not f(a)):7d}")

print()
print("network-based, threshold 7 — pointlessly blocked:",
      [(a["no"], a["net_score"]) for a in AK
       if policy(a) and not network_based(7)(a)])
print("network-based, threshold 9 — missed:",
      [(a["no"], a["net_score"]) for a in AK
       if not policy(a) and network_based(9)(a)])
print("forbidden flows the host-based mechanism never sees:",
      sum(1 for a in AK if not policy(a) and a["dest"] not in HOST_SEEN))
```

```
flow 40 | intent passes 22 | seen by the host-based mechanism 21
network score: permitted 2-8, forbidden 7-13

mechanism        threshold correct false admit  false deny blocked
network-based            6      27           0          13      31
network-based            7      33           0           7      25
network-based            8      32           4           4      18
network-based            9      35           5           0      13
network-based           10      33           7           0      11
network-based           11      31           9           0       9
network-based           12      26          14           0       4
host-based               6      28           8           4      14
host-based               7      29           8           3      13
host-based               8      30          10           0       8
host-based               9      29          11           0       7
combined                 8      35           1           4      21
combined                 9      38           2           0      16

network-based, threshold 7 — pointlessly blocked: [(7, 8), (11, 7), (20, 7), (21, 8), (24, 8), (30, 8), (38, 7)]
network-based, threshold 9 — missed: [(2, 7), (12, 7), (31, 8), (36, 7), (37, 7)]
forbidden flows the host-based mechanism never sees: 8
```

#### The Two Directions of Tightening the Threshold

The network-based system's scores fall in **2–8** for permitted flows, **7–13** for
forbidden ones. The overlap band consists of exactly two values: **7 and 8**. The two
lists in the lower table confirm this — all seven pointlessly blocked flows, and all five
missed flows, score 7 or 8. **No flow outside the band produces an error.**

When the threshold is pulled to **7**, false admit drops to **0**. Every forbidden flow is
blocked; flawless on paper. The cost shows up in the other column: **7** permitted flows
are blocked for nothing. When the threshold opens to **9**, false deny drops to **0** this
time and false admit climbs to **5**. Tightening down to **6** raises false deny to
**13**; opening up to **12** raises false admit to **14**.

The two columns are not zeroed at the same time and cannot be — because the scores of
flows inside the overlap band are indistinguishable from each other. This is different
from the situation in the previous two lessons: there, the two-way error was the result of
a **misbuilt** set, and the correct set zeroed both. Here there is no correct threshold;
there is only a decision about **which error is preferred**. **Zeroing one direction hides
the other**, and as long as the hidden direction is not measured, the mechanism looks
flawless.

The peak of the correct column is at threshold **9**, at **35**. The point where the
errors are **equal** is threshold 8 (4 and 4), and correct there is **32**. Equalizing the
two does not maximize total correctness, because the two classes are not equal in size:
intent passes **22** flows and blocks **18**. Which one is more expensive is not something
the measure can say; it is something policy says.

#### The Floor of Visibility

The host-based system produces a sharper score, but its false admit does not drop below
**8** at any threshold. The reason is not in the threshold: **19** of the forty flows are
outside its field of view, and **8** of these are flows intent blocks. There is no
decision about an unseen flow, and no error — only passage. **The one error a threshold
cannot repair is the error born from an unseen event.**

When the two run together, the table changes. At threshold 9, the network-based system
misses **5** flows, the host-based one misses **11**; the two together miss **2**. False
deny is still **0** and correct climbs to **38** — a number neither system reaches alone.
The gain does not come from combining as such but from **the blind spots not
overlapping**: the flow the network-based system loses in noise, the host-based system
sees sharply, and the zone the host-based system never sees, the network-based system
sees. The remaining **2** missed flows are where both are wrong, and adding more
mechanisms does not close that; changing the criterion does.

#### The Difference Between Detection and Prevention

Every row so far is the same in both modes. An **intrusion prevention system** cuts the
flow; a detection system passes the flow and writes a record. The false-admit and
false-deny counts come out the same in both, because both give the same decision. What
changes is the **cost** of that decision, and the cost sits in the `blocked` column.

In prevention mode this column is the **cut flow** count: at threshold 6, **31** of the
forty flows are cut, and **13** of them should not have been. In detection mode the same
column is the **alert** count: 31 records are written, 13 are **false alarms**, and a
person has to read all of them.

This distinction directly affects the threshold decision. Pulling the threshold from 9 to
6 recovers the **5** missed flows; in exchange, the blocked count climbs from **13** to
**31** — **2.4 times**. In prevention mode this means eighteen extra flows cut; in
detection mode, eighteen extra records. The same measure, two different bills.

#### Surfaces Left Open, and Narrowings

**First surface — missed flows.** At threshold 9, five forbidden flows pass; with the two
mechanisms combined, two pass, and neither has a record behind it. In a set of forty, two
flows are a share of **0.050**, twice the measurement band; it cannot be dismissed.
**Narrowing:** not lowering the threshold — that grows the other column — but **changing
the criterion**. All of these flows have fields the rule chain already distinguishes
exactly; a threshold-based mechanism is placed **alongside** the rule chain, not **in
place of** it.

**Second surface — the unseen zone.** Eight of the nineteen flows the host-based system
does not see are forbidden. The system makes no decision about these eight flows, and a
decision not made produces no record either; to the auditing party, silence and
cleanliness look the same. **Narrowing:** report the **field of view itself** — write
down, in flow counts, which zones are monitored and which are not. A measurement whose
scope is unknown cannot be read.

**Third surface — the alert pile.** Tightening the threshold raises alerts from 13 to 31,
and thirteen of the eighteen extra records are false alarms. An unread alert does the same
job as an unwritten one; a tight threshold can give back its own gain this way.
**Narrowing:** choose the threshold not as a single number but separately by zone — tight
for flows landing in the management zone, loose for flows talking to the outside. The
measurement directly supports this: the difference between the two systems already varied
by zone.

#### Summary

- Because a threshold-based mechanism's criterion is continuous, the two classes' values
  overlap in a band; in this measurement the band is **7–8**, and the entire two-way
  error is born inside it.
- In the network-based system, at threshold **7** false admit is **0** and false deny is
  **7**; at threshold **9** false deny is **0** and false admit is **5**. The two columns
  cannot be zeroed at once.
- The threshold that equalizes the errors (4 and 4 at 8) does not maximize total
  correctness; the classes are not equal in size, and which error is more expensive is
  policy's decision, not the measure's.
- The host-based system's false admit never drops below **8** at any threshold, because
  it never sees **19** of the forty flows; **the one error a threshold cannot repair is
  born from an unseen event.**
- Combined, the two give **38** correct at threshold 9; the gain comes from the blind
  spots not overlapping rather than from combining as such, and the remaining **2** missed
  flows are where both are wrong.
- Detection and prevention produce the same numbers but charge different costs: the
  `blocked` column is cut flows in prevention, alerts that must be read in detection —
  pulling the threshold from 9 to 6 grows it **2.4 times**.

#### Next Step

All four lessons in this topic shared a single assumption: the outside is outside. The
rule chain, the zone graph, and the threshold all decided by looking at **where a flow
comes from**, and the name `ext` was synonymous with "untrusted" in all three. But what if
an outside endpoint has to be let in? Someone working from home, a whole segment at
another site, or two networks talking over a path we do not control — none of these step
outside `ext`, yet all of them are expected to behave like `int`. The next lesson builds
the mechanism that lets this endpoint in, and asks the same two columns again: how are the
destinations to be let in chosen, and what happens to the ones that are not.

### VPN Technologies

Source: https://academia.sh/en/courses/wireless-and-security/vpn-technologies

A tunnel is a routing arrangement: the tunnel selector decides which destinations it lets inside, and a flow that is not selected exits without ever seeing the boundary. Across forty flows, four selectors and two tunnel configurations measure false admit and false deny separately.

The course's four lessons so far used the boundary in the same direction throughout: to
separate the outside from the inside. Firewall types, the rule chain, the zone graph, and
the detection mechanism all took the distinction between "what is inside" and "what is
outside" as given, and built their decision on top of that distinction.

But what if an outside endpoint **has to be let in**? A remote employee's machine, an
entire segment at another site, or a cluster inside a cloud provider — none of these is
physically inside, and yet they need to count as inside. **VPN (virtual private network)**
is the name of the technology class that does this job. This lesson's question is not what
the tunnel protects: it is **which flows the tunnel lets inside**, and what happens to the
ones it does not.

#### A Tunnel Is a Routing Decision

What can be done once the tunnel is established — the scope of access — was measured in
the Perimeter Defense topic of the Cybersecurity curriculum and is not repeated here. What
is measured here is the decision **before** the tunnel is established, and that decision
is not a security decision but a **routing** decision.

The software running at the remote end answers a single question for every flow to be
sent: does this flow go through the tunnel, or through the local exit? If the answer is
yes, the flow is wrapped and carried to the concentrator at the other end of the tunnel;
it is opened there and enters **the organization's boundary from that point**. If the
answer is no, the flow is never wrapped, exits directly through the remote end's own
connection, and **never sees** the organization's boundary at all.

The distinction between these two paths carries the entire lesson. A flow going through
the tunnel is subject to the rule chain. A flow not going through the tunnel is subject to
no rule at all — because it never passes through the place where the rule was written.

#### The Tunnel Selector

The structure that makes this decision is called the **tunnel selector**. A selector is
not a rule chain; it produces no action, it only defines a set: which destinations enter
the tunnel.

```text
# taught dump , not executed

tunnel definition A
  configuration : remote access
  selector      : destination zone { int }
  assigned zone : int
  not selected  : local exit

tunnel definition B
  configuration : site-to-site
  selector      : destination zone { int, dmz, mgmt }
  assigned zone : (none; source zone is preserved)
  not selected  : local exit
```

A setup where the selector covers every destination is called **full tunnel**; one that
leaves part of them out is called a **split tunnel**. The split tunnel's rationale is
carrying capacity: routing all of the remote end's traffic through the concentrator means
routing flows with no relevance at all through it too. What this lesson asks is not
whether the rationale holds, but in which direction its cost is paid.

#### A Selector Is a Set of Paths

Seeing where the selector sits makes the rest of the lesson readable. In the Switching and
Routing course, a packet's path was determined by the **next hop**, chosen by looking at
its destination. The tunnel selector sits in exactly the same place: the tunnel interface
is one of the next-hop candidates, and the selector says for which destinations that
candidate wins. This is why the selector is evaluated **before the rule chain** — the rule
chain waits for the packet to reach the boundary; the selector decides whether the packet
sets out toward the boundary at all.

This has two consequences. First, the selector's resolution is routing's resolution: it
looks at the destination. A distinction like "this port through the tunnel, that port
through the local exit" requires telling apart two flows going to the same destination,
and a set of paths cannot express that. Second, the selector sits at the remote end. The
concentrator sends it when the tunnel is established, but what determines the path of a
non-selected flow is the remote end's own routing table. Loosening the selector at the
remote end is a configuration flaw, and it gives **no indication at all** on the boundary
side: the boundary never sees that flow in the first place.

#### Two Configurations: Site-to-Site and Remote Access

The difference between the two tunnel configurations is not in the selector; it is in
**the identity of the source**.

In a **site-to-site** tunnel, both ends are real zones. One site's internal segment
connects to another's; every flow arrives with its own zone, and the boundary evaluates it
by that zone. The tunnel only changes the path; it does not change who the flow is.

In a **remote-access** tunnel, the remote end has no zone of its own. It is a single
machine, it connects from a variable location, and the boundary does not know which zone
to place it in. This is why the tunnel **assigns** it a zone: every flow coming out of the
tunnel enters the boundary with the zone the concentrator gives it. The assignment is not
a convenience; it is a necessity — the rule chain reads the `source` criterion, and it
needs something to read.

The measurement's assumptions:

- **NS51** — The forty flows are produced from the course's shared fixture; each flow's
  source, destination, and port come from the fixture. The oracle is the `policy`
  function, known because we wrote the fixture. `policy` is **not changed** in this
  lesson.
- **NS52** — A flow entering the tunnel is opened at the concentrator and enters the
  boundary from there; the boundary evaluates it with `policy`. In the remote-access
  configuration, evaluation is done on a record whose source has been **replaced with the
  assigned zone**. What is changed is the record, not the function.
- **NS53** — A flow not entering the tunnel goes out through the remote end's local exit.
  If its destination is the external zone it arrives; if it is headed for one of the
  internal zones it never arrives at all. These flows never pass through the rule chain.
- **NS54** — Four selectors are compared: full tunnel and three split sets. The selectors
  look only at **the destination zone**; they do not use the oracle.
- **NS55** — In the remote-access configuration, the assigned zone is a second axis and is
  measured on its own; the selector is held fixed at full tunnel in that measurement.
- **NS56** — The set is forty flows; the smallest measurable difference is
  `1/40 = 0.025`. A smaller difference cannot be defended with this set.

#### Measurement

```python
"""A tunnel is a routing decision: the selector decides which destinations come inside.

Part 1 - four tunnel selectors, in two tunnel configurations.
Part 2 - what the zone a remote-access tunnel assigns does on its own.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")


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": (80, 443, 22, 3306, 8080)[r(5)]})
    return out


def policy(a):
    """Intent: only the DMZ 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


SELECTORS = {"full tunnel": lambda a: True,
             "split: int": lambda a: a["dest"] == "int",
             "split: int, dmz": lambda a: a["dest"] in ("int", "dmz"),
             "split: except ext": lambda a: a["dest"] != "ext"}


def site_to_site(selector):
    """Both ends are real zones; the tunnel only picks the path."""
    def run(a):
        if selector(a):
            return policy(a)
        return a["dest"] == "ext"      # a flow not selected exits directly
    return run


def remote_access(selector, assigned="int"):
    """The remote end has no zone of its own; the tunnel assigns it one."""
    def run(a):
        if selector(a):
            return policy({**a, "source": assigned})
        return a["dest"] == "ext"
    return run


ak = flows()
print(f"flow {len(ak)} | intent passes "
      f"{sum(1 for a in ak if policy(a))} | destination external "
      f"{sum(1 for a in ak if a['dest'] == 'ext')}")
print()
print(f"{'tunnel selector':<20s} {'selected':>8s} {'site-to-site':>25s}"
      f"{'remote access':>26s}")
print(f"{'':20s} {'':8s} {'correct':>8s} {'f.admit':>8s} {'f.deny':>8s}"
      f" {'correct':>8s} {'f.admit':>8s} {'f.deny':>8s}")
for name, s in SELECTORS.items():
    x, y = diff(ak, policy, site_to_site(s)), diff(ak, policy, remote_access(s))
    print(f"{name:<20s} {sum(1 for a in ak if s(a)):8d} "
          f"{x['correct_pass'] + x['correct_block']:8d} {x['false_admit']:8d} "
          f"{x['false_deny']:8d} {y['correct_pass'] + y['correct_block']:8d} "
          f"{y['false_admit']:8d} {y['false_deny']:8d}")

print()
print(f"{'assigned zone':<14s} {'correct':>6s} {'false admit':>12s} "
      f"{'false deny':>12s}   (full tunnel, remote access)")
for assigned in ZONES:
    d = diff(ak, policy, remote_access(SELECTORS["full tunnel"], assigned))
    print(f"{assigned:<14s} {d['correct_pass'] + d['correct_block']:6d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")

print()
print(f"{'not selected':<20s} {'count':>5s} {'exits directly':>15s} "
      f"{'unreachable':>11s}")
for name, s in SELECTORS.items():
    outside = [a for a in ak if not s(a)]
    print(f"{name:<20s} {len(outside):5d} "
          f"{sum(1 for a in outside if a['dest'] == 'ext'):15d} "
          f"{sum(1 for a in outside if a['dest'] != 'ext'):11d}")
```

```
flow 40 | intent passes 22 | destination external 11

tunnel selector      selected              site-to-site             remote access
                               correct  f.admit   f.deny  correct  f.admit   f.deny
full tunnel                40       40        0        0       25       11        4
split: int                 10       24        6       10       21        9       10
split: int, dmz            18       30        6        4       25       11        4
split: except ext          29       34        6        0       25       11        4

assigned zone  correct  false admit   false deny   (full tunnel, remote access)
ext                22            0           18
dmz                20            0           20
int                25           11            4
mgmt               22           18            0

not selected         count  exits directly unreachable
full tunnel              0               0           0
split: int              30              11          19
split: int, dmz         22              11          11
split: except ext       11              11           0
```

#### Where an Unselected Flow Goes

The upper table's first row is a baseline. In a **site-to-site full tunnel**, all forty
flows go through the tunnel, each enters the boundary with its own zone, and the result is
**40 correct, 0 false admit, 0 false deny**. Here the tunnel added nothing to policy and
took nothing away; it only carried the remote segment in front of the boundary.

As the selector narrows, both directions start speaking at once, and **the two do not
come from the same source**.

**False deny** comes from flows not taken into the tunnel whose destination is an internal
zone. They try to go by the local exit, fail to reach the organization, and intent would
have passed a portion of them. **10** for the `int` selector, **4** for `int, dmz`, **0**
for `except ext`. Widening the selector directly fixes this direction.

**False admit**, however, stays **fixed at 6** no matter how wide the selector gets. Its
source is in the lower table's last row: in all three split configurations, **11** of the
flows not entering the tunnel have an external destination, and these eleven really do
arrive through the local exit. The oracle would have blocked **6** of them — because
policy does not pass every flow with an external destination. Widening the selector from
`int` to `except ext` never touches these six, because none of the three selectors takes
an external destination into the tunnel.

The pattern is this: **in a split tunnel, false deny is born from the selector's
narrowness, and false admit is born from what is left outside the selector never seeing
the rule chain at all.** The first closes as the selector is widened; the second does
not — closing it requires the selector to take in **the external destination too**,
meaning abandoning the split tunnel and returning to full tunnel. The `except ext` row
shows this exactly: false deny is zeroed, false admit is still **6**.

The size of the numbers is also worth noting. In a set of forty flows, the smallest
measurable difference is `1/40 = 0.025`. The fixed six false admits amount to `0.150` in
this set; the `int` selector's ten false denies amount to `0.250`; both are well above the
band. Every nonzero row difference in the table is at least **two flows**, that is,
`0.050`; no reading rests on a single flow. What the measurement claims, however, is not
the numbers themselves but **that the two directions come from separate sources**: one
from the narrowness of the selector, the other from what is left outside the selector
never passing through the audit at all.

#### What the Assigned Zone Does on Its Own

The three columns on the right measure the same selectors in the **remote-access**
configuration, and the table changes: even at full tunnel, **25 correct, 11 false admit,
4 false deny**. Although all forty flows go through the tunnel, the result degrades,
because in this configuration flows enter the boundary not with their own zone but with
**the zone the tunnel assigns**. The rule chain reads the `source` criterion, and it now
reads the same value for every one of them.

The middle table opens up this whole axis. With the assigned zone `int`, **11 false
admit, 4 false deny**. If the assignment is pulled to `ext`, false admit drops to **0**
and false deny climbs to **18**: no flow coming from the tunnel is trusted at all. If the
assignment is opened to `mgmt`, false admit is **18**, false deny is **0**: every flow has
been placed in the widest zone. `dmz`, meanwhile, worsens both directions at once — **0**
and **20** — because the path policy gives the DMZ is a narrow one and does not match the
real distribution of remote ends.

None of the four rows gives zero-zero. The reason is structural: the forty flows come
from **four separate source zones**, and a single assignment collects them into a single
zone. Information carrying a distinction, once mapped to a single value, cannot be
recovered. **The remote-access tunnel's cost is not the bytes it carries but the
distinction it erases.**

The only way to recover the erased distinction is to stop the assignment from being **a
single value**: set up a separate tunnel for each privilege set and assign each tunnel its
own zone. The measurement says this directly — flows from four zones under four separate
assignments produce four separate rows, and none of the rows carries another's error. The
cost shows up on the rule side: as many concentrator interfaces as tunnels, as many
`source` values as interfaces, and that many separate branches in the rule chain. The
course already said that counting rules does not measure policy; here the reverse is also
true — **avoiding a rule does not measure policy either, it just closes off a distinction
cheaply.**

#### Summary

- Before it is established, a tunnel is a **routing** decision: the tunnel selector
  determines which destinations are let inside, and an unselected flow never passes
  through the rule chain.
- In a site-to-site full tunnel, forty flows give **40 correct, 0 false admit, 0 false
  deny**; the tunnel adds nothing to policy.
- In a split tunnel, false deny comes from the selector's narrowness and drops
  **10 → 4 → 0** as it widens; false admit stays fixed at **6** across all three split
  configurations, because its source is the **11** externally destined flows left outside
  the selector.
- In the remote-access configuration, the tunnel must assign the flow a zone; even at full
  tunnel, the result falls to **25 correct, 11 false admit, 4 false deny**.
- The assigned zone alone pulls the two directions in opposite ways: **0 / 18** with
  `ext`, **18 / 0** with `mgmt`. Collecting flows from four zones into a single zone
  erases the distinction irrecoverably.

#### Next Step

The remote-access tunnel's whole trick is making an outside endpoint look **as if it came
from the inside** to the rule chain. The reason this trick is necessary sits in a single
line: the rule chain still reads the `source` criterion and expects a zone to read. The
next lesson measures what happens when this expectation is removed — how rule count and
the two-way error change when the `source` criterion is stripped from the rule chain, and
what has to be put in the location criterion's place.

### Zero Trust Network Model

Source: https://academia.sh/en/courses/wireless-and-security/zero-trust-network-model

When the location criterion is stripped from the rule chain, one rule turns into an implicit allow, three rules get shadowed, and false admit climbs from zero to eighteen; when the identity criterion takes its place, rule count rises from seven to ten, and when the credential condition is added, false deny climbs from one to four.

The remote-access tunnel's whole trick was making an outside endpoint look as if it came
from the inside to the rule chain. The reason that trick was necessary sat in a single
line: the rule chain reads the `source` criterion and expects a zone to read. The tunnel
makes up a zone, because the question the chain asks is "where are you coming from."

**Zero trust** proposes dropping this question. The principle itself was established in
the Cybersecurity curriculum and is not repeated here. This lesson's question is not the
principle; it is **surgical**: when the `source` criterion is cut out of the rule chain,
what is left in the chain, how many rules get shadowed, how does the two-way error change,
and what has to be put in the emptied place?

#### What the Location Criterion Represented

The course's narrow rule set consists of seven rules, and all seven carry the `source`
criterion. The criterion says which zone a flow comes from, and for the rule's author, the
zone is a shorthand: "a machine on the internal segment" and "the organization's employee"
have been treated as the same thing.

This shorthand has no cost — as long as the shorthand is correct. If the machine at the
end of the cable really is the employee's machine, location stands in for identity, and
the chain decides correctly. The previous lesson's remote-access tunnel existed exactly to
keep this shorthand standing: the remote end had no zone, the tunnel assigned it one, and
the shorthand started working again.

Zero trust's claim is that the shorthand no longer holds. What this lesson measures is not
whether the claim is true — it is what **removing** the shorthand does to the rule chain.

#### Stripping the Criterion

The stripping is mechanical: the `source` key is removed from every rule's criteria
dictionary. Rule count does not change, actions do not change, order does not change.

```text
# taught dump , not executed

location-criteria chain                    location criterion stripped
1  permit  source=ext  dest=dmz  port=80    1  permit  dest=dmz  port=80
2  permit  source=ext  dest=dmz  port=443   2  permit  dest=dmz  port=443
3  permit  source=dmz  dest=int  port=3306  3  permit  dest=int  port=3306
4  permit  source=mgmt                      4  permit  (no criteria left)
5  permit  source=int  dest=dmz             5  permit  dest=dmz
6  permit  source=int  dest=ext             6  permit  dest=ext
7  permit  source=int  dest=int             7  permit  dest=int
(implicit deny at the end)                  (implicit deny at the end)
```

The fourth rule carries the whole problem. In its location-criteria form it had a single
criterion, and that criterion was `source`; when stripped, **no criterion is left**. A
criteria-less rule matches every flow: under the "first match wins" rule, the fourth line
is now an **implicit allow**, and it shadows the three rules after it. Implicit deny,
meanwhile, never gets a turn at all.

#### What Takes Its Place

The location criterion indirectly said **on whose behalf** a flow was opened. When it is
removed, a criterion that says this directly has to take its place. This criterion travels
with the flow itself and is not tied to the cable; in this lesson's fixture, its name is
`identity`, and its values are the classes `guest`, `employee`, `service`, and `admin`.

A second field is required: `credential`. Identity is a claim, and there has to be
something that verifies the claim. A flow without a credential has its identity treated as
unknown, and the chain drops it to implicit deny. This is the rule chain's counterpart to
zero trust's sentence "verify, do not assume" — and its cost will be measured.

The identity-criteria chain is **longer** than the location-criteria one. The reason is
the fourth rule: `source=mgmt` opened every destination in a single line, because the zone
was already a trust declaration. On the identity side there is no such declaration; every
destination the `admin` identity can reach is written separately, and one rule becomes
four.

The measurement's assumptions:

- **NS57** — The forty flows are produced from the course's shared fixture. The oracle is
  the `policy` function and is **not changed** in this lesson; intent is the course's
  constant.
- **NS58** — `policy` is written in the language of location. Measuring an
  identity-criteria chain against this oracle is counting the flows where **the two
  languages diverge**; the measurement gives the size of the change, not the correctness
  of identity.
- **NS59** — An identity class is added to every flow. For most flows identity matches the
  zone; the fixture deliberately diverges a minority, because identity is not tied to the
  cable.
- **NS60** — A `credential` status is added to every flow. The fixture leaves a minority
  without one; the credential field says whether the flow's identity claim can be
  verified, not what the claim contains.
- **NS61** — Four chains are compared: location-based, location stripped, identity-based,
  identity and credential gated. The evaluation mechanism is the same in all four: first
  match wins, implicit deny sits at the end.
- **NS62** — The shadowed-rule count is the number of rules fully covered by a criteria
  set that came before them; a rule whose criteria set has been emptied shadows every rule
  after it.
- **NS63** — The set is forty flows; the smallest measurable difference is
  `1/40 = 0.025`.

#### Measurement

```python
"""The effect of stripping the location criterion out of the rule chain.

Part 1 - source-criteria chain, stripped chain, identity-criteria chain.
Part 2 - the gap the stripped criterion leaves in the rule chain.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
IDENTITIES = ("guest", "employee", "service", "admin")
MAPPING = {"ext": "guest", "dmz": "service", "int": "employee", "mgmt": "admin"}


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": (80, 443, 22, 3306, 8080)[r(5)]})
    return out


def add_identity(flows_list, seed=SEED + 6):
    """Adds an identity class and a credential status to each flow."""
    r, out = generator(seed), []
    for a in flows_list:
        divergent, candidate = r(9) == 0, IDENTITIES[r(4)]
        out.append({**a, "identity": candidate if divergent else MAPPING[a["source"]],
                    "credential": r(8) != 0})
    return out


def policy(a):
    """Intent: only the DMZ 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


def shadowed_count(rules):
    count = 0
    for i, (_, criteria) in enumerate(rules):
        for _, earlier in rules[:i]:
            if all(criteria.get(k) == v for k, v in earlier.items()):
                count += 1
                break
    return count


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"})]

STRIPPED = [(e, {k: v for k, v in o.items() if k != "source"}) for e, o in NARROW]

IDENTITY = [(True, {"identity": "guest", "dest": "dmz", "port": 80}),
            (True, {"identity": "guest", "dest": "dmz", "port": 443}),
            (True, {"identity": "service", "dest": "int", "port": 3306}),
            (True, {"identity": "admin", "dest": "ext"}),
            (True, {"identity": "admin", "dest": "dmz"}),
            (True, {"identity": "admin", "dest": "int"}),
            (True, {"identity": "admin", "dest": "mgmt"}),
            (True, {"identity": "employee", "dest": "dmz"}),
            (True, {"identity": "employee", "dest": "ext"}),
            (True, {"identity": "employee", "dest": "int"})]


def credential_gated(rules):
    """A flow without a credential falls to implicit deny."""
    chain = rule_chain(rules)
    return lambda a: a["credential"] and chain(a)


ak = add_identity(flows())
print(f"flow {len(ak)} | intent passes {sum(1 for a in ak if policy(a))} "
      f"| identity diverges from location "
      f"{sum(1 for a in ak if a['identity'] != MAPPING[a['source']])} | no credential "
      f"{sum(1 for a in ak if not a['credential'])}")
print()
print(f"{'rule chain':<24s} {'rules':>5s} {'shadowed':>8s} {'correct':>7s} "
      f"{'false admit':>12s} {'false deny':>12s}")
for name, rules, device in (
        ("location-based", NARROW, rule_chain(NARROW)),
        ("location stripped", STRIPPED, rule_chain(STRIPPED)),
        ("identity-based", IDENTITY, rule_chain(IDENTITY)),
        ("identity + credential", IDENTITY, credential_gated(IDENTITY))):
    d = diff(ak, policy, device)
    print(f"{name:<24s} {len(rules):5d} {shadowed_count(rules):8d} "
          f"{d['correct_pass'] + d['correct_block']:7d} {d['false_admit']:12d} "
          f"{d['false_deny']:12d}")

print()
print("stripped chain's criteria:")
for i, (_, o) in enumerate(STRIPPED):
    print(f"  {i + 1}. {o if o else '(no criteria left)'}")

print()
print("flows whose identity diverges from location (no, location, identity, dest, intent, identity chain):")
for a in ak:
    if a["identity"] != MAPPING[a["source"]]:
        print(f"  {a['no']:2d} {a['source']:8s} {a['identity']:9s} {a['dest']:8s} "
              f"{str(policy(a)):5s} {str(rule_chain(IDENTITY)(a)):5s}")
print("flows without a credential that intent would pass:",
      sum(1 for a in ak if not a["credential"] and policy(a)))
```

```
flow 40 | intent passes 22 | identity diverges from location 2 | no credential 5

rule chain               rules shadowed correct  false admit   false deny
location-based               7        0      40            0            0
location stripped            7        3      22           18            0
identity-based              10        0      38            1            1
identity + credential       10        0      35            1            4

stripped chain's criteria:
  1. {'dest': 'dmz', 'port': 80}
  2. {'dest': 'dmz', 'port': 443}
  3. {'dest': 'int', 'port': 3306}
  4. (no criteria left)
  5. {'dest': 'dmz'}
  6. {'dest': 'ext'}
  7. {'dest': 'int'}

flows whose identity diverges from location (no, location, identity, dest, intent, identity chain):
  34 ext      admin     ext      False True 
  40 int      service   ext      True  False
flows without a credential that intent would pass: 3
```

#### The Cost of Stripping

The second row carries the lesson's harshest number. Rule count stayed at **7**, not one
rule was added, not one was removed — and the chain fell from **40 correct to 22**. False
admit climbed from **0** to **18**; eighteen of the forty flows passed although intent
would have blocked them. That is a share of `0.450` in a set of forty, eighteen times the
measurement band.

False deny, meanwhile, stayed at **0**, and this is not good news — it is the other face
of the same flaw. Because the fourth rule matches every flow, no flow can reach the
implicit deny; the chain no longer carries a path along which it can say "block." **An
audit counting only one direction would have declared this chain flawless** — false deny
zero, no user complaints.

The shadowed-rule count is **3**, and the lower dump gives the reason: the fourth line's
criteria set is empty, an empty set covers every criteria set, and so the fifth, sixth,
and seventh rules are never evaluated. Rule shadowing here is born not from a writing
mistake but from **the removal of a field**.

The pattern is this: **the location criterion can, on its own, be a rule's entire
load-bearing part.** Strip it from such a rule, and no rule is left — well, technically a
rule remains. Rule count does not show this — the chain is still seven lines. The course
already said in Access Control Lists that counting rules does not measure policy; here a
sharper version of the same sentence appears: **counting criteria does not measure policy
either.**

#### The Two Directions of the Identity Criterion

The third row gives the identity-criteria chain: **10 rules, 0 shadowed, 38 correct, 1
false admit, 1 false deny.** Rule count rose from seven to ten, and the entire increase
comes from the fourth rule — the implicit trust declaration the zone carried had to be
written as four separate lines on the identity side. **This is the rule cost of removing
the location criterion.**

The two errors can be read knowing exactly which of the forty flows they are born on; the
lower dump shows both. Flow thirty-four comes from the external zone but carries the
`admin` identity: intent would have blocked it, the identity chain passed it — **false
admit**. Flow forty comes from the internal segment but carries the `service` identity:
intent would have passed it, the identity chain blocked it — **false deny**.

These two numbers are not a flaw; they are **the measure of where the two languages
diverge**. The oracle is written in the language of location; the identity-criteria chain
is written in a different language, and the two languages say the same thing on
thirty-eight of the forty flows and diverge on two. The flows where they diverge are
exactly the flows where the location shorthand was already wrong. So the measurement does
not say "identity decided wrong"; it **gives the number of flows whose intent itself needs
rewriting**, and that number is `2/40 = 0.050`.

The rule that comes out of this is clear: **changing the criterion means changing the
policy, not the rule.** Trying to enforce an intent written in the language of location
with a chain written in the language of identity produces an error on every flow where
the two languages diverge. The move to zero trust is not a rule-chain edit; it is a
rewriting of intent.

#### The Cost of the Credential Condition

The fourth row adds a single condition: a flow without a credential falls to implicit
deny. False admit stays at **1** — the credential condition never touches this direction,
because flow thirty-four has a credential and its identity passes. False deny, however,
climbs from **1** to **4**.

The source of the three-flow rise is in the bottom line: **5** of the forty flows have no
credential, and intent would have passed **3** of them. Correct drops from **38** to
**35**. That is a share of `3/40 = 0.075`, three times the band; a measurable cost.

The two directions do not mix here, and that they do not is the lesson's result. **The
credential condition only grows false deny.** Making verification mandatory means not
passing what cannot be verified, and some portion of what cannot be verified is always
legitimate. Widening credential coverage recovers these three flows; loosening the
coverage — that is, passing an uncredentialed flow by looking at location — brings the
stripped criterion back through the side door and partially recalls the second row's
error.

#### Summary

- The location criterion was identity's shorthand in the rule chain; as long as the
  shorthand is correct it costs nothing, and the narrow set gives **40 correct, 0 false
  admit, 0 false deny** on forty flows.
- When the criterion is mechanically stripped, rule count stays at **7**, but the rule
  whose only criterion was `source` turns into an **implicit allow**: **3** rules get
  shadowed, correct falls to **22**, false admit becomes **18**, and false deny stays at
  **0** — an audit counting only one direction would have declared this chain flawless.
- When the identity criterion takes its place, rule count rises from **7** to **10**; the
  entire increase comes from the zone's implicit trust declaration being written as four
  separate lines.
- The identity-criteria chain gives **38 correct, 1 false admit, 1 false deny**; the two
  errors are the two flows where identity and location diverge, and they measure **the
  number of flows whose intent needs rewriting**, not identity's wrongness.
- The credential condition grows only one direction: false deny climbs from **1** to
  **4**, because intent would have passed **3** of the **5** uncredentialed flows. This is
  the cost of making verification mandatory.

#### Next Step

Six controls were built across the course: generation and coverage on the wireless side,
and segmentation, the rule chain, detection, the tunnel, and the criterion moving from
location to identity on the security side. Each measured its own boundary in both
directions at once. One question is left, and the course's last lesson asks it: when all
six controls are switched on together, which class of network attacks actually closes, how
many classes remain open, and what is there in hand for the ones that stay open?

### Common Network Attacks

Source: https://academia.sh/en/courses/wireless-and-security/common-network-attacks

When the six controls built in this course are switched on together, the number of flows on which each of four attack classes closes is counted; with the narrow selector none of them closes across all forty flows, with full tunnel only one closes, and denial of service stays open on twenty-two flows.

Six controls were built across the course, and each measured its own boundary in both
directions at once: generation and coverage on the wireless side; segmentation, the rule
chain, detection, the tunnel, and the criterion moving from location to identity on the
security side. One question is left: when these six controls are switched on **together**,
which class of network attacks actually closes, how many classes remain open, and what is
there in hand for the ones that stay open? What is measured is not attack; it is **the
scope of defense**.

#### What This Lesson Does Not Do

**How** eavesdropping, spoofing, on-path attack, and denial of service are carried out is
not this course's subject. All four were covered by procedure across eight lessons in the
Network Attacks topic of the Cybersecurity curriculum; procedure, tooling, payload, and
evasion stay there. Here the four names appear only as **class names**, and what stands in
for them is not a procedure but a **condition** — a sentence, read only from the defending
side, that says for which flow the class stays accessible.

| Class | Read from the defending side |
|---|---|
| Eavesdropping | Is the flow's content readable in transit |
| Spoofing | Can the flow's source claim be verified |
| On-path attack | Is content readable **and** the source unverifiable |
| Denial of service | Does the service sit somewhere the flow can reach |

A class is **closed** for a flow if its condition does not hold; it is **open** if the
condition holds.

#### A Control Being On Is Not the Same as a Class Closing

The real distinction this measurement carries is this: a control being **set up** is not
the same as a class being **closed for that flow**. The tunnel is set up, but it does not
touch a flow left outside its selector. The identity criterion is set up, but it produces
no decision for a flow with no credential. The generation requirement is in force, but it
has nothing to do with a flow that has no wireless leg. This is why the measurement is
done at the flow level: forty flows and four classes, **160 flow-class pairs**.

The measurement's assumptions:

- **NS64** — The forty flows are produced from the course's shared fixture; the oracle is
  the `policy` function and is **not changed** in this lesson. Two fields are added to
  each flow: `wireless`, saying whether the first leg is on the wireless segment;
  `credential`, saying whether the source claim can be verified. Both come from the
  fixture and preserve the same flow set as the previous lesson's draws.
- **NS65** — Eavesdropping closes if the flow is inside the tunnel selector **and**, if it
  has a wireless leg, the generation requirement is in force; it does not close if either
  leg is uncovered.
- **NS66** — Spoofing closes if the zero-trust criterion is on **and** the flow has a
  credential. On-path attack requires both to close at once.
- **NS67** — Denial of service closes only when the service is unreachable: segmentation
  and the rule chain are both on, and intent already blocks that flow. On a flow that
  stays reachable, this class is open **by definition**.
- **NS68** — The detection system closes no class; this is not a gap in the setup but the
  mechanism's definition, and contributing zero to the measurement is the expected
  result.
- **NS69** — Two tunnel selectors are compared: narrow selector and full tunnel. In the
  coverage-report measurement, the oracle is closure computed at the flow level.
- **NS70** — The set is 160 pairs; the smallest measurable difference is `1/160`. In rows
  read at the flow level, the resolution is `1/40 = 0.025`.

#### Measurement

```python
"""The scope of defense: which control closes which class, what stays open.

Part 1 - closure at the flow level, across two tunnel selectors.
Part 2 - the flow-class pairs lost when a control is switched off.
Part 3 - the coverage report's two-way error.
"""
SEED = 20260811
ZONES = ("ext", "dmz", "int", "mgmt")
CLASSES = ("eavesdropping", "spoofing", "on-path attack", "denial of service")
CONTROLS = ("segmentation", "rule chain", "tunnel", "detection", "zero trust",
            "wireless generation")


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": (80, 443, 22, 3306, 8080)[r(5)]})
    return out


def add_surfaces(flows_list, seed=SEED + 6, env=SEED + 11):
    """Whether the flow has a wireless leg, whether its source claim has a credential."""
    r, o, out = generator(seed), generator(env), []
    for a in flows_list:
        r(9), r(4)      # identity draws; keeps the same flow set
        out.append({**a, "credential": r(8) != 0, "wireless": o(3) == 0})
    return out


def policy(a):
    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 closure(a, enabled, selector):
    """Which class is closed per flow. Only the defending side is read."""
    eavesdropping = ("tunnel" in enabled and selector(a)
                     and (not a["wireless"] or "wireless generation" in enabled))
    spoofing = "zero trust" in enabled and a["credential"]
    unreachable = ("segmentation" in enabled and "rule chain" in enabled
                   and not policy(a))
    return {"eavesdropping": eavesdropping, "spoofing": spoofing,
            "on-path attack": eavesdropping and spoofing,
            "denial of service": unreachable}


SELECTORS = {"narrow selector": lambda a: a["dest"] in ("int", "dmz"),
             "full tunnel": lambda a: True}
NARROW = SELECTORS["narrow selector"]
ALL = set(CONTROLS)

ak = add_surfaces(flows())
pairs = [(a, s) for a in ak for s in CLASSES]
print(f"flow {len(ak)} | class {len(CLASSES)} | flow-class pair {len(pairs)}"
      f" | with a wireless leg {sum(a['wireless'] for a in ak)} | credentialed "
      f"{sum(a['credential'] for a in ak)} | crossing a zone boundary "
      f"{sum(a['source'] != a['dest'] for a in ak)}")
print()
print(f"{'class':<18s} {'narrow selector':>18s} {'full tunnel':>18s}")
print(f"{'':18s} {'closed':>9s} {'open':>8s} {'closed':>9s} {'open':>8s}")
for s in CLASSES:
    h = []
    for sec in SELECTORS.values():
        k = sum(closure(a, ALL, sec)[s] for a in ak)
        h += [k, len(ak) - k]
    print(f"{s:<18s} {h[0]:9d} {h[1]:8d} {h[2]:9d} {h[3]:8d}")

print()
whole = sum(closure(a, ALL, NARROW)[s] for a, s in pairs)
print(f"{'control switched off':<22s} {'closed pairs':>12s} {'lost':>6s}")
print(f"{'(none)':<22s} {whole:12d} {0:6d}")
for c in CONTROLS:
    n = sum(closure(a, ALL - {c}, NARROW)[s] for a, s in pairs)
    print(f"{c:<22s} {n:12d} {whole - n:6d}")

print()
REPORTS = {"control checklist": lambda a, s: s != "denial of service",
           "boundary only": lambda a, s: closure(a, ALL - {"wireless generation",
                                                            "zero trust"},
                                                  NARROW)[s]}
print(f"{'coverage report':<18s} {'correct':>7s} {'false admit':>12s} "
      f"{'false deny':>12s}")
for name, report in REPORTS.items():
    d = diff(pairs, lambda c: closure(c[0], ALL, NARROW)[c[1]],
             lambda c: report(c[0], c[1]))
    print(f"{name:<18s} {d['correct_pass'] + d['correct_block']:7d} "
          f"{d['false_admit']:12d} {d['false_deny']:12d}")
```

```
flow 40 | class 4 | flow-class pair 160 | with a wireless leg 9 | credentialed 35 | crossing a zone boundary 27

class                 narrow selector        full tunnel
                      closed     open    closed     open
eavesdropping             18       22        40        0
spoofing                  35        5        35        5
on-path attack            16       24        35        5
denial of service         18       22        18       22

control switched off   closed pairs   lost
(none)                           87      0
segmentation                     69     18
rule chain                       69     18
tunnel                           53     34
detection                        87      0
zero trust                       36     51
wireless generation              79      8

coverage report    correct  false admit   false deny
control checklist       91           51           18
boundary only          105            0           55
```

#### What Closes and What Does Not

The upper table places the four classes side by side in two tunnel configurations, and
**with the narrow selector, no class closes on all forty of the forty flows.** The best
row is spoofing: **35 closed, 5 open**. Eavesdropping is **18/22**, on-path attack
**16/24**, denial of service **18/22**.

When the tunnel selector covers every destination, eavesdropping becomes **40 closed, 0
open** — **one class closes entirely.** Its cost was measured in the tunnel lesson:
closing a class entirely requires the control to be applied **without exception**, and the
price of that exceptionlessness is paid on the rule side. On-path attack, being an
intersection, is always narrower (**16** and **35**); the two controls need to cover **the
same set of flows**.

Denial of service stays at **18/22** in both columns, and this fixedness is structural:
this class closes only when the service is unreachable, and intent itself passes **22** of
the forty flows. **Reachability is the reason this class cannot be closed**, not a gap in
configuration.

The middle table shows what each control carries. With all six controls on, **87**
flow-class pairs are closed. Switching off zero trust opens **51** pairs; the tunnel,
**34**; segmentation or the rule chain, **18**; wireless generation, **8**. Switching off
detection opens **0** pairs: detection closes no class; it gives visibility along with its
two-way error, and the set it sees is the **27** flows crossing a zone boundary.

#### The Two Directions of the Coverage Report

The lower table carries the lesson's central warning. A coverage report that **looks at
the control checklist** — one that says "the tunnel is set up, zero trust is set up,
generation is in force, so three classes are closed" — gives **91 correct, 51 false
admit, 18 false deny**. On fifty-one pairs it calls something closed that is not; on
eighteen pairs it fails to see something that is closed, because it does not account for
denial of service narrowing through the rule chain.

A report counting only the controls **at the boundary** — one that treats generation and
zero trust as another team's job and leaves them out — gives **105 correct, 0 false
admit, 55 false deny**. It has no false admits at all; its entire error comes from
undercounting.

Both reports are wrong, and **their wrongness runs in opposite directions**: one shows
coverage as wider than it is, the other as narrower. **A coverage report that counts only
one direction hides the other.**

#### Classes Left Open, and Narrowings

In the narrow-selector configuration, all four classes leave open flows. What is left in
hand must be written next to each of them.

**Eavesdropping — 22 flows open.** What stays open are the flows outside the tunnel
selector. **Narrowing:** widen the selector; at full tunnel, open drops to **0**, and its
cost was measured in the tunnel lesson. On the **9** flows with a wireless leg, coverage
depends on both conditions at once.

**Spoofing — 5 flows open.** What stays open are flows with no credential. **Narrowing:**
widen credential coverage. Dropping an uncredentialed flow to implicit deny grew false
deny; widening coverage is the one move that improves both numbers together.

**On-path attack — 24 flows open.** Being an intersection, the widest opening is here.
**Narrowing:** **align** the two controls' coverage sets; matching the tunnel selector to
credential coverage gains more than widening a single control does.

**Denial of service — 22 flows open, and no configuration closes it.** **Narrowing:** the
surface, with segmentation and the rule chain together, drops from **40** to **22**; the
remaining twenty-two are flows intent passes and that stay reachable because that is the
service's job. What remains of this class's surface narrows **on the capacity side, not
the rule side**, and that side is not this course's subject.

#### Summary

- The four classes appear in this lesson only as **class names**; their procedures stay in
  the Network Attacks topic of the Cybersecurity curriculum. Here each class is reduced to
  a condition read from the defending side, and the measurement is done on **160
  flow-class pairs**.
- A control being set up and a class closing are separate things: with the narrow
  selector, spoofing is **35/5**, eavesdropping **18/22**, on-path attack **16/24**,
  denial of service **18/22**, and no class closes on all forty flows at once.
- At full tunnel, only eavesdropping rises to **40/0**; a class closing entirely requires
  the control to be applied without exception, and its cost was measured in the tunnel
  lesson.
- With all six controls on, **87** pairs are closed; zero trust carries **51**, the
  tunnel **34**, segmentation and the rule chain **18**, generation **8**. Detection
  closes **0** pairs, and this is by definition.
- The coverage report's two directions err separately: the report reading the control
  checklist produces **51 false admits**, the report counting only the boundary produces
  **55 false denies**. Denial of service closes under no configuration; the remaining
  **22** flows narrow on the capacity side.

#### Course Wrap-Up

The course asked a single question fourteen times: **what is the difference between the
boundary the design intended and what actually crosses it?** Every lesson built one
mechanism, changed one thing, and counted the difference in both directions at once. The
table below collects the boundary each lesson measured and both of its directions; the
numbers come from each lesson's own run.

| Lesson | Boundary Measured | False Admit | False Deny |
|---|---|---|---|
| Physics of Wireless Transmission | cell intent of thirty meters at power 52 | 0 | 4 |
| Access Points and Controllers | location of the decision: standalone → controller | 2 → 0 | 4 → 4 |
| Channel Planning and Coverage | power budget 58 → 40 | 5 → 0 | 1 → 14 |
| Roaming and Band Steering | handover threshold 30 → 14 | 8 → 0 | 0 → 8 |
| Wireless Security Protocols | generation condition 1 → 3 | 5 → 2 | 1 → 20 |
| Short-Range and Wide-Area Wireless | short / local / wide range | 3 / 0 / 1 | 0 / 4 / 2 |
| Mobile Networks | inter-site distance 90 → 30 | 5 → 0 | 1 → 0 |
| Firewall Types | unmirrored / mirrored / stateful / application-aware | 6 / 6 / 0 / 0 | 7 / 4 / 0 / 8 |
| Access Control Lists | narrow 7 / wide 6 / misordered 8 / tightened 7 rules | 0 / 2 / 2 / 0 | 0 / 0 / 0 / 2 |
| Network Segmentation and DMZ | zone graph 2 → 4, narrow / wide derivation | 0 / 6 → 0 / 0 | 22 / 0 → 0 / 0 |
| Intrusion Detection and Prevention | network-based threshold 7 → 9; 19 flows the host-based system never sees | 0 → 5 | 7 → 0 |
| VPN Technologies | tunnel selector: full tunnel → split | 0 → 6 | 0 → 10 |
| Zero Trust Network Model | location criterion stripped → identity criterion | 18 → 1 | 0 → 1 |
| Common Network Attacks | coverage report: control checklist / boundary only | 51 / 0 | 18 / 55 |

The table has a single reading, and it shows up in all fourteen rows: **the two columns do
not move together.** Every adjustment that zeroes one column grows the other. Lower the
power budget and the leak stops while the miss count rises to fourteen; tighten the
generation condition and the leak drops from five to two while the miss count rises to
twenty; narrow the tunnel selector and the unreachable count becomes ten. A control that
counted only one direction would have shown a flawless table in all fourteen of the
fourteen lessons.

The second reading concerns rule count. Seven rules gave 40/40 correct, seven rules gave
38; six rules leaked, eight rules both leaked and shadowed. **Counting rules does not
measure policy** — and stripping a criterion can turn a rule into an implicit allow
without changing the rule count at all. The reason two rows are left blank sits in the
same place: in this course, the source of a number is not a shared definition, it is **the
lesson that ran it**.

This is also where the course leaves off. Fourteen lessons measured how the boundary gets
**written**: which power, which threshold, which rule chain, which criterion, which
selector. None of them asked what happens to the boundary after it is written. A written
boundary sits inside a living system: its load changes, its rules get edited by hand, its
deviation is invisible to anyone, and its two-way error is known only once it is measured.
**Writing the boundary and operating what was written are separate jobs.**

The next course, **Network Operations and Automation**, takes on the second job: how
traffic gets distributed, which measurements the network uses to report its own state, and
which failure modes close when configuration is managed as code. The two columns counted
in this course become, there, a measurement problem: **how is a boundary's two-way error
seen while it is running?**

