# VPN networks: address space, routes and endpoint

> How a VPN network's tunnel CIDR, dial-in endpoint, pushed routes and TOTP policy are stored, served to the engine, and handed to a device.

A **network** is the address space, the route set and the dial-in endpoint that
every device on a TeleQuick VPN inherits. It is the first object you create
in the VPN console, because nothing downstream is meaningful without it: a
`vpn_user` row carries a `NOT NULL network_id`, and the config file or connect
command handed to a device is rendered from its network's endpoint host. Create
users before a network exists and the portal can show credentials it has no way
to tell anyone how to use.

The console screen lists networks (`adminVpn.listNetworks`) and writes them
through a single `adminVpn.upsertNetwork` mutation — the same procedure creates
and edits, with `id` present only when editing. Saving a network also
invalidates the user list, because users are read with their network joined in
order to render that connect command.

Validity does not disable the **Create network** / **Save changes** button.
Clicking an invalid form reveals the per-field messages instead.

## What a network is

| Field | Stored as | Role |
| ----- | --------- | ---- |
| Name | `name` | Label for the network in lists and on user rows. Up to 64 characters. Required. |
| Tunnel CIDR | `cidr` | The tunnel address space, including the server's own address. Required. |
| Endpoint host | `endpoint_host` | The hostname clients dial. Required. |
| Port | `endpoint_port` | The port clients dial. Defaults to `443`. |
| WebTransport path | `wt_path` | The path clients dial. Defaults to `/vpn`. |
| MTU | `mtu` | Tunnel MTU pushed to clients. Defaults to `1350`. |
| Pushed routes | `routes` | CIDRs sent to every client in the assign frame. |
| DNS servers | `dns` | Resolver addresses sent to every client in the assign frame. |
| Isolate clients | `isolate_clients` | Blocks device-to-device traffic inside the tunnel. |
| Require a second factor | `require_mfa` | Requires TOTP at connect time. |
| Grace period | `mfa_grace_until` | Absolute instant until which unenrolled users are exempt. |

Both list fields — routes and DNS — are entered as one text field, separated by
commas or whitespace. Empty entries are dropped, so
`10.0.0.0/8, 192.168.10.0/24` and `10.0.0.0/8 192.168.10.0/24` are equivalent.
Each entry is validated individually and the first failing entry is named in
the error message.

## Tunnel CIDR and the engine's vpn.network value

The **Tunnel CIDR** you type is copied straight into the engine's `vpn.network`
knob. That is why it must *include the server's own address*:

```
10.88.0.1/24   ✅ the server is 10.88.0.1, the /24 is the tunnel space
10.88.0.0/24   ❌ names the network number, not the server
```

This is the single field on the screen that is easiest to get subtly wrong, so
the hint sits inline under the input rather than in a document nobody opens.

The field is validated in two steps. First, the value must carry a prefix at
all — a bare IP names no address space and returns
`Enter a CIDR block (10.88.0.0/24)`. Then the shared CIDR validator checks the
block itself. The value is required; a network with no address space cannot be
saved.

## Endpoint host, port and WebTransport path

These three fields are what a device dials, and together they are what the
portal renders into a device config or connect command.

- **Endpoint host** is validated as a hostname and is required. Leave it blank
  and a device can be issued a working credential and still have nothing to
  connect to — which is why the console refuses to save without it.
- **Port** may be left blank; the save defaults it to `443`. A non-empty value
  must be a valid port (1–65535).
- **WebTransport path** must start with `/`, otherwise the field reports
  `Path must start with /`. Blank is accepted and the save defaults it to
  `/vpn`.

Changing any of the three changes the config handed to *every* device on the
network the next time it is rendered, because the config is generated from the
network row rather than stored per device.

## Pushed routes, DNS and MTU in the assign frame

When a client connects, the engine answers with an **assign frame**. The
network's routes, DNS servers and MTU travel in that frame, so all three are
policy set once per network rather than per device.

- **Pushed routes** are the CIDRs the client should send through the tunnel.
  Leave the field blank to route nothing but the tunnel itself.
- **DNS servers** are plain IP addresses, for example `10.88.0.1, 1.1.1.1`.
  Each entry is validated as an IP.
- **MTU** must be between 576 and 1500. The default is `1350`.

## Client isolation

**Isolate clients from each other** is a single checkbox on the network. With
it enabled, devices still reach the routed networks you pushed, but they cannot
reach each other's tunnel addresses. The networks table flags isolation on the
row so you can see at a glance which address spaces are segmented.

## Requiring a second factor (TOTP)

**Require a second factor (TOTP)** makes every device on the network present an
authenticator code in addition to its token. The flag is stored on the network
(`require_mfa`), so it applies to all users whose `network_id` points at it.

Enrolment is per user. The policy is evaluated at connect time against the
user's enrolment state, which means turning the toggle on is a policy change,
not a migration: users who have already enrolled are held to their authenticator
immediately, and users who have not are handled by the grace window described
below.

## Grace periods and the mfa_not_enrolled refusal

When the requirement is on, the engine refuses a connection from a user who has
not enrolled a TOTP secret with **`mfa_not_enrolled`**. That refusal is
correct, and from the perspective of the person holding the laptop it is
indistinguishable from an outage. Plan for it.

The **grace period** field softens the rollout:

- Blank or `0` enforces immediately.
- A positive number of days (up to 365) sets an exemption window. During the
  window, users who have **not** enrolled keep connecting with their token
  alone. Users who **have** enrolled are held to their authenticator right away.
- Once the window passes, unenrolled users are refused with
  `mfa_not_enrolled`.

The window is persisted as an **absolute instant** (`mfa_grace_until`), not as a
duration, and the console is deliberate about when it recomputes that instant:

| What you did | What is written |
| ------------ | --------------- |
| Turned the requirement off | `null` — no window |
| Left the days field untouched and saved | The stored instant, verbatim |
| Typed a new number of days | A fresh window starting now |
| Set the days field to `0` or blank | `null` — enforce immediately |

Editing an unrelated field — an extra pushed route, say — and saving must not
quietly extend a security grace that is already counting down, so the stored
instant is kept unless you actually touched the days field.

The remaining days shown when you reopen a network are derived from the stored
instant and **rounded up**, so a window with six hours left reads `1` rather
than `0 days` while it is still open. Once the instant has passed, the field
reads blank.

## MFA readiness before you save

Requiring a second factor is also how you lock out your whole team in one
click. So the screen names the affected users *before* the toggle is saved,
using `adminVpn.mfaReadiness({ orgId, networkId })`. The panel appears
underneath the grace-period field and reports:

| From the procedure | Shown as |
| ------------------ | -------- |
| `activeUsers` | `N of M active users enrolled` |
| `enrolled` | The enrolled count in that same line |
| `notEnrolled` | The count **and the name of every user** it would affect |
| `kekConfigured` | A warning when the server has no signing KEK |

The wording changes with the grace setting:

- **No grace window.** The panel border turns destructive and reads that those
  users *will be refused as soon as you save*, followed by their names.
- **Grace window set.** The same users are listed, with the note that they keep
  connecting until the grace period ends and are refused after that.
- **Nobody outstanding.** The panel confirms that enforcing now locks nobody
  out.

Readiness is only knowable for a network that already exists. A brand new
network has no users yet, so the panel is not shown while creating one.

## Sealing enrolled secrets: STREAM_SIGNING_KEK_B64

`mfaReadiness` also reports whether the server has a signing key-encryption key
configured. When `kekConfigured` is false, the readiness panel prints a
destructive-coloured warning: **enrolled TOTP secrets are stored unsealed.**

Set `STREAM_SIGNING_KEK_B64` on the server so that enrolled secrets are sealed
at rest, and do that before relying on the second-factor requirement in
production. The requirement itself will function without the KEK — the engine
still enforces TOTP and still refuses unenrolled users — but the secrets it
checks against are not protected.

## Tags

Networks are taggable as the `vpn_network` resource type. The screen resolves
tags for every visible network in one bulk call and writes them per row, and the
tag filter at the top of the page narrows the table, showing how many of the
total networks matched.

## Field validation reference

| Field | Rule |
| ----- | ---- |
| Name | Required. Up to 64 characters. |
| Tunnel CIDR | Required. Must contain a `/` prefix and be a valid CIDR block. |
| Endpoint host | Required. Must be a valid hostname. |
| Port | Optional. If present, a valid port (1–65535). Defaults to `443`. |
| WebTransport path | Optional. Must start with `/`. Defaults to `/vpn`. |
| MTU | Integer between 576 and 1500. Defaults to `1350`. |
| Pushed routes | Each entry must be a valid CIDR. The first bad entry is named. |
| DNS servers | Each entry must be a valid IP. The first bad entry is named. |
| Grace period | Only validated when the requirement is on. Integer between 0 and 365. |
