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

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