docs: four-section /docs/ restructure with new-user content, accuracy pass, and gateway feature-set rewrite

Restructures /docs/ by reader purpose (tutorials, how-to,
reference, design), adds the new-user-progression and
operator-recipe content the prior layout lacked, runs an
accuracy pass against current source across the pre-existing
design docs, and rewrites the gateway feature-set documentation
end-to-end around its actual operational profile (a niche
feature designed for systems already serving DHCP/DNS to a
LAN, with two independent halves — outbound LAN→mesh, inbound
mesh→LAN — sharing one nftables table, one binary, and one
control socket). Top-level README and getting-started rewritten
around two equally-weighted deployment modes (overlay on
existing IP networks; ground-up over non-IP transports).

## Additions

- 11 new tutorials in docs/tutorials/: an 8-step new-user
  progression from single-daemon test-mesh peering through
  to a ground-up two-device mesh, an IPv6-adapter side-trip
  walkthrough, an Advanced Tutorials index, and a hand-held
  OpenWrt walk-through for fips-gateway deployment that
  exercises both halves of the feature.
- 12 new how-tos in docs/how-to/: firewall activation,
  Nostr discovery (resolve / advertise / open across five
  scenarios), Tor onion (directory + control_port modes),
  UDP buffer tuning, unprivileged-user setup, persistent
  identity, host aliases, Bluetooth LE peering, MTU
  diagnostics, manual Linux-host gateway deployment (covers
  both halves), gateway troubleshooting (organised by half),
  and a section index.
- 9 new reference docs in docs/reference/: configuration,
  wire formats, control-socket protocol, four CLI references
  (fips, fipsctl, fipstop, fips-gateway), security posture
  matrix, and Nostr events catalog. Configuration and
  wire-formats are renamed-and-extended from prior design/
  versions; the other seven are net-new.
- 6 new design docs: fips-concepts, fips-architecture, and
  fips-prior-work split out of the deleted fips-intro.md;
  consolidated fips-mmp and fips-mtu aggregations; and a
  new generic port-advertisement-and-nat-traversal doc
  (Nostr-signaled port advertisement plus UDP NAT-traversal
  protocol, FIPS as an example implementation, suitable for
  eventual NIP submission).
- Top-level docs/getting-started.md walking through the
  binary-installer-only Install story.
- packaging/common/hosts pre-populated with the eight public
  test-mesh nodes so shortnames resolve out of the box on
  every fresh install.

## Changes

- 23 wire-format diagrams relocated to reference/diagrams/
  alongside the wire-formats move.
- 4 design diagrams corrected against source code
  (fips-protocol-stack, fips-identity-derivation,
  fips-coordinate-discovery, fips-routing-decision).
- 10 pre-existing design docs reconciled with current
  source. Numeric corrections: stale link-MMP report bounds
  (now [1s, 5s] with 200 ms cold-start floor); UDP default
  MTU (now 1280, IPv6 minimum); node_addr formula
  (SHA-256(pubkey)[..16]); Noise patterns (IK at link, XK
  at session); peer-ACL semantics (strict allowlist requires
  ALL in peers.deny); daemon DNS upstream ([::1]:5354);
  on-the-wire bloom-filter size (1,071 bytes); obsolete
  Cargo-feature references (PR #79 dropped them) removed.
- Transport framing tightened across the docs: TCP is for
  UDP-filtered networks (not NAT traversal); Tor is a
  deployment mode (not failover); WebSocket dropped (not a
  shipped FIPS transport); WiFi promoted to Implemented via
  Ethernet in infrastructure mode; classic-Bluetooth row
  removed (BLE is the only Bluetooth-mode transport).
- docs/design/fips-gateway.md rewritten end-to-end to lead
  with the niche-feature framing and the two-halves
  structure. Title moved from "FIPS Outbound LAN Gateway"
  to "FIPS Gateway"; architecture section describes the
  common machinery (the fips-gateway service, the nftables
  table, the control socket) before splitting into separate
  "Outbound Half" and "Inbound Half" sections of equal
  weight; security considerations split per-half; no Future
  Work section (speculative directions live in the project
  tracker, not in protocol design docs). Inbound port
  forwarding is a first-class half rather than a buried
  "Implemented Extensions" subsection.
- Gateway terminology unified across all gateway docs as a
  separate Linux service running alongside the fips daemon
  (its own systemd unit / OpenWrt init script). Container-
  pattern terms (sidecar) are reserved for the
  Docker/Kubernetes sidecar deployment examples — the
  testing/sidecar/ tree, examples/k8s-sidecar/,
  examples/sidecar-nostr-relay/,
  examples/wireguard-sidecar-macos/, and the related
  CHANGELOG / top-level README entries — where the term
  carries its standard container meaning.
- Net-new design body content: rekey section in
  fips-mesh-layer (Noise IK msg1/msg2 over the established
  link, K-bit cutover, drain window, smaller-NodeAddr-wins
  tie-breaker on dual-init); Mesh Size Estimation and
  Antipoison FPR Cap sections in fips-bloom-filters;
  Mesh-Interface Query Filter subsection in
  fips-ipv6-adapter; failure-suppression knobs and clock-
  skew tolerance in fips-nostr-discovery; loop-rejection
  and mid-chain ancestor swap added to spanning-tree
  propagation / stability rules; Priority Chain in
  fips-mesh-operation renumbered to match the
  routing-decision diagram.
- Top-level README: dropped the stale nostr-discovery
  cargo-feature parenthetical. docs/README.md and the four
  section READMEs (tutorials, how-to, reference, design)
  refreshed for the new structure; index rows reflect both
  halves of the gateway feature and the new fips-gateway
  CLI reference.
- Cargo.toml [package.metadata.deb] assets path updated for
  the fips-security.md move; .gitignore /reference/ rule
  anchored to repo root so docs/reference/ is trackable.
- packaging/openwrt-ipk/files/etc/fips/fips.yaml
  configuration-doc URL updated to the new
  docs/reference/configuration.md location.

## Deletions

- docs/design/fips-intro.md (split into the three new intro
  design docs).
- docs/design/document-relationships.svg (orphan, no longer
  referenced).
- docs/proposals/ tree removed; the only proposal it
  contained (the Nostr UDP hole-punch protocol) was
  rewritten as the new generic
  design/port-advertisement-and-nat-traversal.md.
This commit is contained in:
Johnathan Corgan
2026-05-08 03:02:12 +00:00
parent 4cdf382038
commit 5abf9a9325
91 changed files with 11474 additions and 3278 deletions
+25
View File
@@ -0,0 +1,25 @@
# Reference
Information-oriented technical descriptions for lookup on demand.
Reference content describes *what is*: wire formats, configuration
keys, command-line flags, control-socket commands, default values,
file paths, exit codes. It is consulted, not read end-to-end.
Reference is austere by design: minimal narrative, no opinions, no
guidance on when to use a feature. The "why" lives in design/; the
"how do I accomplish X" lives in how-to/.
## Available Reference
| Document | Scope |
| -------- | ----- |
| [wire-formats.md](wire-formats.md) | All FMP and FSP message byte layouts, encapsulation walkthrough |
| [configuration.md](configuration.md) | Full YAML configuration reference for the daemon and gateway |
| [security.md](security.md) | nftables baseline, peer ACL, cryptographic primitives, rekey defaults, threat-resistance matrix |
| [nostr-events.md](nostr-events.md) | Kind 37195 advert, Kind 21059 traversal signaling, Kind 10050 inbox relays |
| [transports.md](transports.md) | Per-transport statistics counter inventory |
| [control-socket.md](control-socket.md) | Line-delimited JSON control protocol for the daemon and gateway |
| [cli-fips.md](cli-fips.md) | `fips` daemon CLI: options, exit codes, environment, files |
| [cli-fipsctl.md](cli-fipsctl.md) | `fipsctl` control-client: subcommands, options, exit codes |
| [cli-fipstop.md](cli-fipstop.md) | `fipstop` live-status TUI: tabs, keybindings |
| [cli-fips-gateway.md](cli-fips-gateway.md) | `fips-gateway` service CLI: options, exit codes, files |
+125
View File
@@ -0,0 +1,125 @@
# `fips-gateway`
Long-running service that bridges a LAN segment into the FIPS mesh.
## Synopsis
```text
fips-gateway [-c FILE] [-l LEVEL]
```
## Description
`fips-gateway` runs alongside `fips` on the same host, reads the same
`fips.yaml`, and exposes two complementary functions to the LAN it
fronts:
- **Outbound (LAN -> mesh).** Allocates a virtual IPv6 from a managed
pool when a LAN client resolves `<npub>.fips`, installs nftables
DNAT/SNAT/masquerade rules so the client's traffic is rewritten and
carried into the mesh through the daemon's `fips0` adapter.
- **Inbound (mesh -> LAN).** Installs nftables DNAT and LAN-side
masquerade rules so mesh-side traffic arriving on `fips0` for the
configured listen ports is rewritten to a LAN `host:port`, per the
`gateway.port_forwards[]` block.
The service runs alongside `fips`, not as a replacement for it:
the daemon must be running on the same host with the TUN adapter
and DNS resolver enabled. The gateway is read-only with respect to the
daemon's state, and connects to the daemon's resolver only — it is
not a peer. For the architecture, see
[../design/fips-gateway.md](../design/fips-gateway.md).
`fips-gateway` is **Linux-only**. The binary errors out and exits with
status `1` on any other platform, since the NAT pipeline is built on
nftables and proxy NDP. See
[Configuration](#configuration) for the platform notes that follow
from this.
## Options
| Flag | Argument | Default | Description |
| ---- | -------- | ------- | ----------- |
| `-c`, `--config` | `FILE` | *(default search paths)* | Use `FILE` as the configuration. Skips the default search paths. |
| `-l`, `--log-level` | `LEVEL` | `info` | Tracing level: `trace`, `debug`, `info`, `warn`, `error`. Overridden by `RUST_LOG` if set (see [Environment](#environment)). |
| `-V` | — | — | Print the short version. |
| `--version` | — | — | Print the long version (short version plus build target triple). |
| `-h`, `--help` | — | — | Print usage and exit. |
## Configuration
`fips-gateway` reads the same `fips.yaml` as `fips`; the gateway is
configured under the top-level `gateway:` block. The block must
include at minimum `enabled: true`, `pool`, and `lan_interface`. For
each field — pool, LAN interface, DNS listener, conntrack overrides,
and inbound `port_forwards[]` — see the
[Gateway section](configuration.md#gateway-gateway) of the
configuration reference.
The same default search paths apply as for `fips`
(see [`fips`](cli-fips.md#files)); `-c FILE` overrides the search.
The gateway must be able to read the same configuration file the
daemon is reading, or the two will disagree about pool, DNS port,
and LAN interface.
For deployment recipes, see
[../how-to/deploy-gateway.md](../how-to/deploy-gateway.md) (manual
Linux host) and
[../tutorials/deploy-fips-gateway.md](../tutorials/deploy-fips-gateway.md)
(OpenWrt walk-through).
## Exit Codes
| Code | Meaning |
| ---- | ------- |
| `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
| `1` | Non-Linux platform, configuration load failure, missing or invalid `gateway:` block, NAT/network setup failure, or control-socket bind failure. The reason is printed to stderr or the log before exit. |
## Environment
| Variable | Description |
| -------- | ----------- |
| `RUST_LOG` | Tracing filter directive. Takes precedence over `--log-level`. Examples: `info`, `debug`, `fips=trace,fips::gateway=debug`. |
## Files
| Path | Purpose |
| ---- | ------- |
| `/etc/fips/fips.yaml` | Gateway configuration (top-level `gateway:` block). Same file the daemon reads. |
| `/run/fips/gateway.sock` | Gateway control socket. Hardcoded path; chowned to group `fips` (mode `0770`) at startup so members of that group can query without sudo. |
| `inet fips_gateway` (nftables) | NAT table the gateway installs and tears down. View with `nft list table inet fips_gateway`. |
The gateway also adds and removes a `local <pool-cidr> dev lo` route
in the local routing table so the kernel accepts pool addresses as
locally-owned.
## Control Socket
`fips-gateway` exposes a JSON line-protocol control socket separate
from the daemon's. The command set (`show_gateway`, `show_mappings`)
and JSON shapes are documented in the
[Gateway Command Catalog](control-socket.md#gateway-command-catalog).
There is no `fipsctl` subcommand for the gateway — query the socket
directly with `nc -U`, or watch the **Gateway** tab in
[`fipstop`](cli-fipstop.md), which polls the gateway socket
automatically.
## See also
- [`fips`](cli-fips.md) — the daemon. Required to be running on the
same host.
- [`fipstop`](cli-fipstop.md) — the live-status TUI; its Gateway tab
polls the gateway control socket.
- [configuration.md § Gateway](configuration.md#gateway-gateway) —
full `gateway.*` block reference.
- [control-socket.md § Gateway Command Catalog](control-socket.md#gateway-command-catalog)
— wire protocol for the gateway socket.
- [../design/fips-gateway.md](../design/fips-gateway.md) — design,
NAT pipeline, virtual IP pool lifecycle.
- [../how-to/deploy-gateway.md](../how-to/deploy-gateway.md) — manual
Linux deployment.
- [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md)
— diagnostic recipes.
- [../tutorials/deploy-fips-gateway.md](../tutorials/deploy-fips-gateway.md)
— OpenWrt walk-through.
+93
View File
@@ -0,0 +1,93 @@
# `fips`
The FIPS mesh network daemon.
## Synopsis
```text
fips [-c FILE]
```
On Windows the same binary additionally accepts `--install-service`,
`--uninstall-service`, and (used internally by the service control
manager) `--service`.
## Description
`fips` is the FIPS daemon. It loads a YAML configuration, resolves an
identity, brings up the TUN adapter, listens on configured transports,
authenticates peers, maintains the spanning tree, and forwards mesh
traffic. There is one daemon per node.
The daemon stays in the foreground, logging to stderr, until it
receives `SIGINT` or `SIGTERM`. On Windows, the service variant is
controlled through the standard service control manager.
## Options
| Flag | Argument | Description |
| ---- | -------- | ----------- |
| `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. |
| `-V` | — | Print the short version (e.g. `0.3.0-dev (rev abcdef1)`). |
| `--version` | — | Print the long version: short version plus build target triple. |
| `-h`, `--help` | — | Print usage and exit. |
| `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. |
| `--uninstall-service` | — | (Windows only) Uninstall the Windows service. Requires Administrator. |
| `--service` | — | (Windows only, internal) Run as a Windows service. Invoked by the service control manager — not for direct use. |
There are no other CLI flags; all daemon behaviour is governed by the
YAML configuration. See [configuration.md](configuration.md).
## Exit Codes
| Code | Meaning |
| ---- | ------- |
| `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
| `1` | Failed to load configuration, resolve identity, construct the node, or start the node. The reason is printed to stderr before exit. |
## Environment
| Variable | Description |
| -------- | ----------- |
| `RUST_LOG` | Tracing filter directive. Overrides `node.log_level` from the config. Examples: `info`, `debug`, `fips=trace,fips::node::handlers::mmp=debug`. |
| `XDG_RUNTIME_DIR` | Used to derive the default control-socket path when `/run/fips` does not exist. See [control-socket.md](control-socket.md). |
| `FIPS_CONFIG` | (Windows service mode only) Path to the configuration file when the daemon runs under the service control manager. |
The daemon also clamps the `nostr_relay_pool`, `nostr_sdk`, and `nostr`
log targets to `info` whenever the effective log level is below
`trace`, so that `RUST_LOG=debug` does not flood the journal with raw
relay frames. To see those frames, set the level to `trace`.
## Files
`fips` looks for `fips.yaml` in the following locations, lowest to
highest priority. All present files are merged in priority order; the
highest-priority value wins.
| Priority | Path | Purpose |
| -------- | ---- | ------- |
| 1 | `/etc/fips/fips.yaml` | System-wide defaults |
| 2 | `~/.config/fips/fips.yaml` | User preferences |
| 3 | `~/.fips.yaml` | Legacy user config |
| 4 | `./fips.yaml` | Deployment-specific overrides |
Adjacent to the highest-priority config file the daemon reads (or
writes, on first start) the identity files:
| File | Mode | Purpose |
| ---- | ---- | ------- |
| `fips.key` | `0600` | Bech32 nsec for the persistent identity (Unix only; Windows inherits parent ACLs). |
| `fips.pub` | `0644` | Bech32 npub corresponding to `fips.key`. |
When `node.identity.persistent` is `false` (the default), a fresh
keypair is written to these files on every start.
The control socket path is derived per
[control-socket.md](control-socket.md).
## See also
- [`fipsctl`](cli-fipsctl.md) — control-socket client.
- [`fipstop`](cli-fipstop.md) — live-status TUI.
- [configuration.md](configuration.md) — YAML reference.
- [control-socket.md](control-socket.md) — control-socket protocol.
+148
View File
@@ -0,0 +1,148 @@
# `fipsctl`
Command-line client for the FIPS daemon's control socket.
## Synopsis
```text
fipsctl [-s SOCKET] <subcommand> [args...]
```
## Description
`fipsctl` connects to a running daemon over its control socket
(Unix domain socket on Linux/macOS, TCP loopback on Windows), sends
one JSON request, and pretty-prints the response. Exits with a
non-zero status if the socket cannot be reached, the daemon returns an
error, or the request times out.
`fipsctl keygen` is a special case: it does not contact the daemon and
operates purely on local files.
For the line-delimited JSON wire protocol, see
[control-socket.md](control-socket.md). For the YAML configuration
that defines the socket location, see
[configuration.md](configuration.md).
## Global Options
| Flag | Argument | Description |
| ---- | -------- | ----------- |
| `-s`, `--socket` | `PATH` | Override the control-socket path (Linux/macOS) or TCP port (Windows). |
| `-V`, `--version` | — | Print the short version. |
| `--version` | — | Print the long version. |
| `-h`, `--help` | — | Print usage and exit. Per-subcommand help via `fipsctl <subcommand> --help`. |
## Subcommands
### `show <what>`
Read-only queries against the daemon. Each subcommand maps 1:1 to a
control-socket query (see [control-socket.md](control-socket.md)) and
prints the response's `data` object as pretty JSON.
| Subcommand | Control-socket command | Returns |
| ---------- | ---------------------- | ------- |
| `show status` | `show_status` | Node-level status: identity, version, peer/link/session counts, TUN state, recent sparklines. |
| `show peers` | `show_peers` | Authenticated peer list with link IDs, transport addresses, MMP metrics, Noise/rekey state. |
| `show links` | `show_links` | Active links (one per FMP-authenticated peer): direction, state, byte counters. |
| `show tree` | `show_tree` | Spanning-tree state: root, my coordinates, parent, peer declarations. |
| `show sessions` | `show_sessions` | End-to-end FSP sessions: state, traffic counters, session-MMP metrics, path MTU. |
| `show bloom` | `show_bloom` | Bloom-filter state: own filter sequence, leaf dependents, per-peer filter summaries. |
| `show mmp` | `show_mmp` | MMP metrics summary: per-peer link-layer metrics and per-session session-layer metrics. |
| `show cache` | `show_cache` | Coordinate cache: TTL, fill ratio, per-destination coords and path MTU. |
| `show connections` | `show_connections` | Pending handshake connections: state, idle time, resend count. |
| `show transports` | `show_transports` | Transport instances: type, state, MTU, local address, per-transport stats. |
| `show routing` | `show_routing` | Routing summary: pending lookups, retry state, forwarding/discovery/error/congestion counters. |
| `show identity-cache` | `show_identity_cache` | Cached `(node_addr → npub)` entries with last-seen timestamps. |
### `acl <what>`
| Subcommand | Control-socket command | Returns |
| ---------- | ---------------------- | ------- |
| `acl show` | `show_acl` | Loaded peer-ACL state: allow/deny files, effective mode, default decision, entry counts. |
### `stats <what>`
Time-series metrics from the in-process history rings.
| Subcommand | Control-socket command | Description |
| ---------- | ---------------------- | ----------- |
| `stats list` | `show_stats_list` | Enumerate available metrics, their units, and the per-ring retention windows. |
| `stats peers` | `show_stats_peers` | List peers tracked in stats history (active or recently active). |
| `stats history <metric> [options]` | `show_stats_history` | Fetch a time-series window for one metric. |
`stats history` options:
| Flag | Argument | Default | Description |
| ---- | -------- | ------- | ----------- |
| `--peer` | `npub` or hostname | *(none)* | Required for per-peer metrics; resolves through `/etc/fips/hosts` if not an npub. |
| `--window` | `<N>s` / `<N>m` / `<N>h` | `10m` | Window duration. |
| `--granularity` | `1s` or `1m` | `1s` | Ring resolution. `1s` uses the fast ring; `1m` uses the slow ring. |
| `--plot` | — | off | Render a Unicode-block sparkline to stdout instead of JSON. |
### `keygen [options]`
Generate a new FIPS identity keypair locally. Does not contact the
daemon.
| Flag | Argument | Default | Description |
| ---- | -------- | ------- | ----------- |
| `-d`, `--dir` | `DIR` | `/etc/fips` (Unix), `%APPDATA%\fips` (Windows) | Output directory for `fips.key` and `fips.pub`. |
| `-f`, `--force` | — | off | Overwrite an existing `fips.key`. |
| `-s`, `--stdout` | — | off | Print `nsec` then `npub` to stdout instead of writing files. |
`fips.key` is written with mode `0600` and `fips.pub` with mode `0644`
on Unix. After running `keygen`, set `node.identity.persistent: true`
in `fips.yaml` or the daemon will overwrite the keys on next start.
### `connect <peer> <address> <transport>`
Tell the daemon to dial a peer over a specific transport.
| Argument | Description |
| -------- | ----------- |
| `peer` | npub (bech32) or hostname from `/etc/fips/hosts`. |
| `address` | Transport endpoint, e.g. `192.168.1.10:2121`, `[2001:db8::1]:2121`, or a Tor onion. FIPS-mesh ULAs (`fd00::/8`) are rejected for the IP-based transports (udp, tcp, ethernet). |
| `transport` | One of `udp`, `tcp`, `tor`, `ethernet`. |
### `disconnect <peer>`
Tell the daemon to drop a peer link.
| Argument | Description |
| -------- | ----------- |
| `peer` | npub (bech32) or hostname from `/etc/fips/hosts`. |
## Exit Codes
| Code | Meaning |
| ---- | ------- |
| `0` | Daemon returned `{"status":"ok",...}`. |
| `1` | Argument parse failure, control-socket connection failure, daemon returned `{"status":"error",...}`, or local I/O failure (keygen). The error message is printed to stderr. |
## Environment
| Variable | Description |
| -------- | ----------- |
| `XDG_RUNTIME_DIR` | Used to derive the default control-socket path when `/run/fips` is absent. |
`fipsctl` does not consume `RUST_LOG`; logging is for the daemon.
## Files
| Path | Purpose |
| ---- | ------- |
| `/etc/fips/hosts` | Maps hostnames to npubs for the `connect`, `disconnect`, and `--peer` arguments. See [configuration.md](configuration.md). |
| Control socket (default) | Same resolution as the daemon: `/run/fips/control.sock` if present, else `$XDG_RUNTIME_DIR/fips/control.sock`, else `/tmp/fips-control.sock` (Unix); TCP `localhost:21210` (Windows). |
If you get `Permission denied` connecting to the socket on Linux,
add your user to the `fips` group (`sudo usermod -aG fips $USER`)
and log out and back in.
## See also
- [`fips`](cli-fips.md) — the daemon.
- [`fipstop`](cli-fipstop.md) — live-status TUI.
- [control-socket.md](control-socket.md) — wire protocol.
- [configuration.md](configuration.md) — YAML reference.
+123
View File
@@ -0,0 +1,123 @@
# `fipstop`
Live-status terminal UI for a running FIPS daemon.
## Synopsis
```text
fipstop [-s SOCKET] [--gateway-socket PATH] [-r SECONDS]
```
## Description
`fipstop` is a `ratatui`-based dashboard. It opens the daemon control
socket, polls a small set of `show_*` queries on a timer, and renders
the state in a tabbed full-screen UI. A separate poll runs against the
gateway control socket when the Gateway tab is active.
`fipstop` is read-only — it cannot mutate daemon state. Use
[`fipsctl`](cli-fipsctl.md) for `connect` / `disconnect` and friends.
## Options
| Flag | Argument | Default | Description |
| ---- | -------- | ------- | ----------- |
| `-s`, `--socket` | `PATH` | (auto) | Daemon control-socket path / port. Same default as `fipsctl`. |
| `--gateway-socket` | `PATH` | (auto) | `fips-gateway` control-socket path / port. Default: `/run/fips/gateway.sock` (Unix), TCP port `21211` (Windows). |
| `-r`, `--refresh` | `SECONDS` | `2` | Poll interval. |
| `-V`, `--version` | — | — | Print short version. |
| `--version` | — | — | Print long version. |
| `-h`, `--help` | — | — | Print usage and exit. |
## Tabs
Tabs cycle in this order. Each tab issues the listed control-socket
query on its first activation and on every refresh tick while active.
| Tab | Query | Shows |
| --- | ----- | ----- |
| **Node** | `show_status` | Identity, version, uptime, peer/link/session counts, sparklines for mesh size, tree depth, peer count, bytes, loss. |
| **Peers** | `show_peers` (+ `show_links`, `show_transports` cross-refs) | Authenticated peers in a table. Selecting a row and pressing Enter opens a detail view. |
| **Transports** | `show_transports` (+ `show_links`, `show_peers` cross-refs) | Tree of transport instances with per-link children when expanded. |
| **Sessions** | `show_sessions` | End-to-end FSP sessions. |
| **Tree** | `show_tree` | Spanning-tree state and per-peer coordinates. |
| **Filters** | `show_bloom` | Per-peer Bloom-filter state. |
| **Performance** | `show_mmp` | Link-layer and session-layer MMP metrics. |
| **Routing** | `show_routing` (+ `show_cache` cross-ref) | Forwarding/discovery counters, pending lookups, retry state. |
| **Graphs** | `show_stats_history` family + `show_stats_peers` | Stacked time-series plots. Three modes: node-level metrics, one metric across peers, all metrics for one peer. |
| **Gateway** | `show_gateway` and `show_mappings` against the gateway socket | Pool utilisation and per-mapping state when `fips-gateway` is running. Empty when the gateway socket is unreachable. |
The cycle order in the UI is: Node → Peers → Transports → Sessions →
Tree → Filters → Performance → Routing → Graphs → Gateway. The Links
and Cache tabs are not in the cycle but are fetched as cross-references
to populate Peers, Transports, and Routing detail views.
## Keybindings
### Global
| Key | Action |
| --- | ------ |
| `q`, `Ctrl-C` | Quit. |
| `Tab` | Next tab. |
| `Shift-Tab` | Previous tab. |
| `g` | Jump to the Graphs tab. |
| `Esc` | Close detail view (if open). |
### Table tabs (Peers, Sessions, Transports, Gateway)
| Key | Action |
| --- | ------ |
| `Up`, `Down` | Move row selection. |
| `Enter` | Open detail view for the selected row. |
### Transports tab (extra)
| Key | Action |
| --- | ------ |
| `Right`, `Space` | Expand the selected transport row to show its links. |
| `Left` | Collapse the selected transport row. |
| `e` | Expand all transports. |
| `c` | Collapse all transports. |
### Graphs tab (extra)
| Key | Action |
| --- | ------ |
| `Up`, `Down` | Scroll within the stacked plots. |
| `Right`, `Space` | Next time window. Cycles `1m / 1s``10m / 1s``1h / 1s``24h / 1m`. |
| `Left` | Previous time window. |
| `m` | Cycle view mode: `Node` (stacked node metrics) → `MetricByPeer` (one per-peer metric across all peers) → `PeerByMetric` (all per-peer metrics for one peer). |
| `n` | Next selector (next per-peer metric in MetricByPeer; next peer in PeerByMetric). |
| `Shift-N` | Previous selector. |
## Exit Codes
| Code | Meaning |
| ---- | ------- |
| `0` | Normal quit. |
| `1` | Failed to initialise the terminal. The reason is printed to stderr. |
A failure to reach the daemon socket is **not** fatal: the dashboard
displays "Disconnected" in the status bar and retries on every refresh
tick.
## Environment
| Variable | Description |
| -------- | ----------- |
| `XDG_RUNTIME_DIR` | Used to derive the default control-socket and gateway-socket paths when `/run/fips` is absent. |
## Files
Same control-socket resolution rules as
[`fipsctl`](cli-fipsctl.md#files). The gateway socket follows the same
pattern with `gateway.sock` in place of `control.sock`, falling back
to `/tmp/fips-gateway.sock` if neither system path nor
`XDG_RUNTIME_DIR` is available.
## See also
- [`fipsctl`](cli-fipsctl.md) — issue mutating commands.
- [`fips`](cli-fips.md) — the daemon.
- [control-socket.md](control-socket.md) — wire protocol fipstop polls.
File diff suppressed because it is too large Load Diff
+166
View File
@@ -0,0 +1,166 @@
# Control Socket Protocol
The FIPS daemon and `fips-gateway` each expose a local control socket
that accepts line-delimited JSON requests and returns line-delimited
JSON responses. `fipsctl` and `fipstop` are clients of this protocol;
operators can also drive it directly with any tool that can speak
length-bounded JSON over a stream socket.
## Connection
### Linux / macOS
A Unix domain socket. The default path is resolved in this order:
1. `/run/fips/control.sock` (or `/run/fips/gateway.sock` for the
gateway), if `/run/fips` exists. This is what the `fips.service`
systemd unit creates.
2. `$XDG_RUNTIME_DIR/fips/control.sock` otherwise.
3. `/tmp/fips-control.sock` if neither of the above is available.
The daemon `chown`s the socket file and its parent directory to the
`fips` group at bind time and sets mode `0770`. Members of the `fips`
group can therefore connect without root. Add a user with
`sudo usermod -aG fips $USER` (re-login required).
The path can be overridden at the daemon side via
`node.control.socket_path` in the YAML config, and at the client side
via `fipsctl -s PATH` or `fipstop -s PATH`.
### Windows
A TCP listener bound to `127.0.0.1`. The daemon's port is `21210` by
default; the gateway's is `21211`. Only loopback connections are
accepted. Override via `node.control.socket_path` (which takes a port
number string on Windows).
Windows TCP does not provide filesystem-level ACLs — any local user
can connect. See the security note in
[configuration.md](configuration.md#control-socket-nodecontrol).
## Request Format
One JSON object per line, terminated by `\n`. Maximum request size is
4096 bytes; longer requests are dropped with `request too large`.
```json
{"command": "<name>", "params": {<object>}}
```
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `command` | string | yes | Command name. See [Daemon command catalog](#daemon-command-catalog) and [Gateway command catalog](#gateway-command-catalog). |
| `params` | object | only for commands that take parameters | Parameter object. Unknown fields are ignored; missing required fields produce an error response. |
Unknown top-level fields in the request are silently ignored.
## Response Format
One JSON object per line.
```json
{"status": "ok", "data": {<object>}}
{"status": "error", "message": "<reason>"}
```
| Field | Type | When present |
| ----- | ---- | ------------ |
| `status` | string | always; one of `"ok"` or `"error"`. |
| `data` | object | on `ok` responses. |
| `message` | string | on `error` responses. |
### I/O timeouts
The daemon enforces a 5-second timeout for both the request read and
the response write. If the connection idles longer than that, the
daemon closes it with no response.
### Common error messages
| Message | Cause |
| ------- | ----- |
| `empty request` | Connection closed before a newline was received. |
| `invalid request: <serde error>` | Malformed JSON or missing `command`. |
| `request too large` | Request exceeded 4096 bytes. |
| `read timeout` / `read error: ...` | Slow client or transport failure. |
| `unknown command: <name>` | Command not registered with this daemon. |
| `missing params for <name>` | Command requires `params` but none were provided. |
| `missing '<field>' parameter` | Required parameter missing. |
| `query timeout` | Internal handler did not respond within 5 seconds. |
| `node shutting down` | Daemon is exiting. |
| `gateway not yet initialized` | (Gateway socket only) snapshot has not been published yet. |
## Daemon Command Catalog
Read-only queries are dispatched in `src/control/queries.rs`;
mutating commands are dispatched in `src/control/commands.rs`. The
table below lists every command currently registered.
### Read-only queries
| Command | Params | `data` shape (top-level keys) |
| ------- | ------ | ----------------------------- |
| `show_status` | — | `version`, `npub`, `node_addr`, `ipv6_addr`, `state`, `is_leaf_only`, `peer_count`, `session_count`, `link_count`, `transport_count`, `connection_count`, `tun_state`, `tun_name`, `effective_ipv6_mtu`, `control_socket`, `pid`, `exe_path`, `uptime_secs`, `estimated_mesh_size`, `forwarding`, `sparklines`. |
| `show_acl` | — | `allow_file`, `deny_file`, `enforcement_active`, `effective_mode`, `default_decision`, `allow_all`, `deny_all`, `allow_file_entries`, `deny_file_entries`, `allow_entries`, `deny_entries`. |
| `show_peers` | — | `peers[]` — per-peer object: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `connectivity`, `link_id`, `direction`, `transport_addr`, `transport_type`, `is_parent`, `is_child`, `tree_depth`, `stats`, `noise`, `current_k_bit`, `mmp`, plus optional `nostr_traversal`, `rekey_in_progress`, `rekey_draining`. |
| `show_links` | — | `links[]``link_id`, `transport_id`, `remote_addr`, `direction`, `state`, `created_at_ms`, `stats`. |
| `show_tree` | — | `my_node_addr`, `root`, `is_root`, `depth`, `my_coords[]`, `parent`, `parent_display_name`, `declaration_sequence`, `declaration_signed`, `peer_tree_count`, `peers[]`, `stats`. |
| `show_sessions` | — | `sessions[]``remote_addr`, `npub`, `display_name`, `state` (`established`, `initiating`, `awaiting_msg3`, `unknown`), `is_initiator`, `last_activity_ms`, `stats`, optional `mmp`, `current_k_bit`, `is_draining`. |
| `show_bloom` | — | `own_node_addr`, `is_leaf_only`, `sequence`, `leaf_dependent_count`, `leaf_dependents[]`, `peer_filters[]`, `stats`. |
| `show_mmp` | — | `peers[]` (link-layer per peer), `sessions[]` (session-layer per session). Each entry includes loss/RTT/ETX/goodput, smoothed values, trends. |
| `show_cache` | — | `count`, `max_entries`, `fill_ratio`, `default_ttl_ms`, `expired`, `avg_age_ms`, `entries[]` — per-destination coords, depth, age, last-used, optional `path_mtu`. |
| `show_connections` | — | `connections[]` — pending handshakes: `link_id`, `direction`, `handshake_state`, `started_at_ms`, `idle_ms`, `resend_count`, optional `expected_peer`. |
| `show_transports` | — | `transports[]``transport_id`, `type`, `state`, `mtu`, `name`, `local_addr`, optional `tor_mode`, `onion_address`, `tor_monitoring`, `stats`. |
| `show_routing` | — | `coord_cache_entries`, `identity_cache_entries`, `pending_lookups[]`, `pending_tun_destinations`, `pending_tun_packets`, `recent_requests`, `retries[]`, `forwarding`, `discovery`, `error_signals`, `congestion`. |
| `show_identity_cache` | — | `entries[]`, `count`, `max_entries`. Each entry: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `last_seen_ms`, `age_ms`. |
| `show_stats_list` | — | `metrics[]` (each with `name`, `unit`, `scope`), `fast_ring_seconds`, `slow_ring_minutes`, `peer_retention_seconds`. |
| `show_stats_history` | `metric` (req), `peer` (req for per-peer metrics), `window` (`<N>s` / `<N>m` / `<N>h`, default `10m`), `granularity` (`1s` / `1m`, default `1s`) | A single `Series`: `metric`, `unit`, `granularity_seconds`, `values[]`. |
| `show_stats_all_history` | `peer` (optional npub), `window`, `granularity` | `granularity_seconds`, `window_seconds`, `peer`, `series[]` (one per metric). |
| `show_stats_peers` | — | `peers[]`, `count`. Each entry: `npub`, `node_addr`, `display_name`, `is_active`, `first_seen_secs_ago`, `last_contact_secs_ago`. |
| `show_stats_history_all_peers` | `metric` (req per-peer name), `window`, `granularity` | `metric`, `unit`, `granularity_seconds`, `window_seconds`, `peers[]` (each with `node_addr`, `display_name`, `is_active`, `values[]`). |
The schema of each query response is pinned by snapshot tests in
`src/control/snapshots/`; intentional schema changes regenerate those
fixtures.
### Mutating commands
| Command | Required params | Behaviour |
| ------- | --------------- | --------- |
| `connect` | `npub` (bech32), `address` (transport endpoint), `transport` (`udp`, `tcp`, `tor`, `ethernet`) | Asks the node to dial the peer over the named transport. Returns the API result on success or an error string on failure. |
| `disconnect` | `npub` (bech32) | Asks the node to drop the link to the named peer. |
Both commands run on the daemon's main task and may block briefly
while the node mutates its state.
## Gateway Command Catalog
`fips-gateway` exposes a separate control socket with its own command
set. Dispatch lives in `src/gateway/control.rs`.
| Command | Params | `data` shape |
| ------- | ------ | ------------ |
| `show_gateway` | — | `pool_total`, `pool_allocated`, `pool_active`, `pool_draining`, `pool_free`, `nat_mappings`, `dns_listen`, `uptime_secs`, `pool_cidr`, `lan_interface`, `dns_upstream`, `dns_ttl`, `pool_grace_period`. |
| `show_mappings` | — | `mappings[]``virtual_ip`, `mesh_addr`, `node_addr`, `dns_name`, `state` (`Allocated`, `Active`, `Draining`), `sessions`, `age_secs`, `last_ref_secs`. |
Until the first snapshot has been published (very early in startup),
both commands return `gateway not yet initialized`.
## Driving the Socket Directly
```sh
# Linux / macOS
echo '{"command":"show_status"}' | sudo nc -U /run/fips/control.sock
# Windows (PowerShell with a TCP-capable tool of your choice)
```
The newline at the end of the request is required: the daemon reads
one line per connection. The connection is closed after the single
response is written.
## See also
- [`fipsctl`](cli-fipsctl.md) — full-featured client.
- [`fipstop`](cli-fipstop.md) — read-only TUI.
- [configuration.md](configuration.md) — `node.control.*` keys.
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 192" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="192" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">AncestryEntry (32 bytes)</text>
<!-- Row 0 (015): node_addr -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">015</text>
<rect x="55" y="36" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">node_addr</text>
<text x="315" y="76" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — routing identifier</text>
<!-- Row 1 (1623): sequence -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">1623</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 2 (2431): timestamp -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">2431</text>
<rect x="55" y="140" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE — Unix seconds</text>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+39
View File
@@ -0,0 +1,39 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 160" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="160" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="290" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP/FSP Common Prefix (4 bytes)</text>
<!-- Bit ruler -->
<text x="65" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="130" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="195" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="325" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="530" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Byte 0: version (high nibble) -->
<rect x="35" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="82" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="82" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<!-- Byte 0: phase (low nibble) -->
<rect x="130" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="177" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="177" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<!-- Byte 1: flags -->
<rect x="225" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="290" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="290" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<!-- Bytes 2-3: payload_len -->
<rect x="355" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="455" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="455" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Byte offset labels -->
<text x="82" y="130" fill="#666" font-size="10" text-anchor="middle">byte 0</text>
<text x="290" y="130" fill="#666" font-size="10" text-anchor="middle">byte 1</text>
<text x="455" y="130" fill="#666" font-size="10" text-anchor="middle">bytes 2-3</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 218" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="218" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">CoordsRequired (0x20) — 34 bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr we couldn't route to</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Note -->
<text x="310" y="212" fill="#777" font-size="10" text-anchor="middle">plaintext error signal (U flag set) · total on wire: 38 bytes (4 prefix + 34 payload)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+18
View File
@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 114" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="114" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Disconnect (0x50) — 2 bytes</text>
<!-- Row 0 (01): msg_type + reason -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x50</text>
<rect x="315" y="36" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="445" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">reason</text>
<text x="445" y="78" fill="#f88aaa" text-anchor="middle" font-size="10">1 byte</text>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,91 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 498" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="498" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Complete Encrypted Frame (37 + body bytes)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: receiver_idx (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="150" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: counter low (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 3: counter high (bytes 12-15) -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">1215</text>
<rect x="55" y="212" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="242" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
<!-- AAD bracket -->
<text x="10" y="166" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 166)">AAD</text>
<line x1="18" y1="56" x2="18" y2="264" stroke="#555" stroke-width="1"/>
<line x1="18" y1="56" x2="23" y2="56" stroke="#555" stroke-width="1"/>
<line x1="18" y1="264" x2="23" y2="264" stroke="#555" stroke-width="1"/>
<!-- Separator: cleartext above / ciphertext below -->
<line x1="55" y1="272" x2="575" y2="272" stroke="#555" stroke-width="1" stroke-dasharray="6,4"/>
<text x="315" y="288" fill="#777" font-size="10" text-anchor="middle">── ciphertext (encrypted with ChaCha20-Poly1305) ──</text>
<!-- Row 4: timestamp (bytes 16-19, encrypted) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">1619</text>
<rect x="55" y="296" width="520" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="338" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 5: msg_type (1 byte) + body starts -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">20</text>
<rect x="55" y="348" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="372" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="390" fill="#f8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="348" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="380" y="374" fill="#666" text-anchor="middle" font-size="11">message body ...</text>
<!-- Row 6: body variable -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="400" width="520" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="430" fill="#666" text-anchor="middle" font-size="11">message body (variable length)</text>
<!-- AEAD tag -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">+16</text>
<rect x="55" y="452" width="520" height="30" fill="#4a2d4a" stroke="#b04ab0" stroke-width="1.5" rx="3"/>
<text x="315" y="472" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Poly1305 AEAD tag (16 bytes)</text>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -0,0 +1,31 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 186" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="186" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Established Frame Inner Header (5 bytes, plaintext after decrypt)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: timestamp (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 1: msg_type (byte 4) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">4</text>
<rect x="55" y="108" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="120" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="150" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 1: body starts -->
<rect x="185" y="108" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="380" y="134" fill="#666" text-anchor="middle" font-size="11">message body ...</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 290" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="290" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Established Frame Outer Header (16 bytes, AEAD AAD)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: receiver_idx (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="132" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="150" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: counter low (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 3: counter high (bytes 12-15) -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">1215</text>
<rect x="55" y="212" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="242" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="270" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FilterAnnounce (0x20) — 11 + filter bytes</text>
<!-- Row 0 (03): msg_type(1) + sequence starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<rect x="185" y="36" width="390" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="380" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<!-- Row 1 (48): sequence continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — monotonic counter</text>
<!-- Row 2 (910): hash_count + size_class -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">910</text>
<rect x="55" y="140" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="185" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">hash_count</text>
<text x="185" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="140" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">size_class</text>
<text x="445" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">1 byte</text>
<!-- Row 3 (11): filter_bits -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">11</text>
<rect x="55" y="198" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="222" fill="#8af8f8" text-anchor="middle" font-size="11">filter_bits (variable, 512 &lt;&lt; size_class bytes)</text>
<!-- Total -->
<text x="310" y="258" fill="#777" font-size="10" text-anchor="middle">v1 payload: 1,035 bytes (11 header + 1,024 filter)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 524" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="524" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FSP Complete Encrypted Message (34 + body bytes)</text>
<!-- Bit ruler -->
<text x="85" y="50" fill="#888" font-size="10" text-anchor="middle">0</text>
<text x="150" y="50" fill="#888" font-size="10" text-anchor="middle">4</text>
<text x="215" y="50" fill="#888" font-size="10" text-anchor="middle">8</text>
<text x="345" y="50" fill="#888" font-size="10" text-anchor="middle">16</text>
<text x="550" y="50" fill="#888" font-size="10" text-anchor="middle">32</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="82" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="56" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="98" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="56" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="98" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="56" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="98" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="56" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="80" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="98" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: counter low (bytes 4-7) -->
<text x="50" y="134" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="108" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="138" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / lower 4 bytes LE</text>
<!-- Row 2: counter high (bytes 8-11) -->
<text x="50" y="186" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="160" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="190" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">counter / upper 4 bytes LE</text>
<!-- AAD bracket -->
<text x="10" y="134" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 134)">AAD</text>
<line x1="18" y1="56" x2="18" y2="212" stroke="#555" stroke-width="1"/>
<line x1="18" y1="56" x2="23" y2="56" stroke="#555" stroke-width="1"/>
<line x1="18" y1="212" x2="23" y2="212" stroke="#555" stroke-width="1"/>
<!-- Optional coords -->
<text x="50" y="238" fill="#666" font-size="10" text-anchor="end">opt</text>
<rect x="55" y="220" width="520" height="40" fill="#1f1f3a" stroke="#4ad99a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="244" fill="#8af8c8" text-anchor="middle" font-size="11">cleartext coordinates (if CP flag set, variable length)</text>
<!-- Separator: ciphertext below -->
<line x1="55" y1="268" x2="575" y2="268" stroke="#555" stroke-width="1" stroke-dasharray="6,4"/>
<text x="315" y="284" fill="#777" font-size="10" text-anchor="middle">── ciphertext (encrypted with ChaCha20-Poly1305) ──</text>
<!-- Row 3: timestamp (4 bytes, encrypted) -->
<text x="50" y="318" fill="#666" font-size="10" text-anchor="end">+03</text>
<rect x="55" y="292" width="520" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="316" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="334" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 4: msg_type (1) + inner_flags (1) + body starts -->
<text x="50" y="370" fill="#666" font-size="10" text-anchor="end">+4</text>
<rect x="55" y="344" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="368" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="386" fill="#f8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="344" width="130" height="52" fill="#3d3d2d" stroke="#a0a04a" stroke-width="1.5" rx="3"/>
<text x="250" y="368" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">inner_flags</text>
<text x="250" y="386" fill="#d8d88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="344" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="445" y="374" fill="#666" text-anchor="middle" font-size="11">body ...</text>
<!-- Row 5: body variable -->
<text x="50" y="422" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="396" width="520" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="426" fill="#666" text-anchor="middle" font-size="11">message body (variable length)</text>
<!-- AEAD tag -->
<text x="50" y="468" fill="#666" font-size="10" text-anchor="end">+16</text>
<rect x="55" y="452" width="520" height="30" fill="#4a2d4a" stroke="#b04ab0" stroke-width="1.5" rx="3"/>
<text x="315" y="472" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Poly1305 AEAD tag (16 bytes)</text>
<!-- Totals -->
<text x="310" y="510" fill="#777" font-size="10" text-anchor="middle">overhead: 34 bytes (12 header + 6 inner + 16 tag) · coords add variable length</text>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

@@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 500" font-family="monospace" font-size="13">
<defs>
<marker id="arrowR" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<polygon points="0,0 10,4 0,8" fill="#e0e0e0"/>
</marker>
<marker id="arrowL" markerWidth="10" markerHeight="8" refX="1" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<polygon points="10,0 0,4 10,8" fill="#e0e0e0"/>
</marker>
</defs>
<!-- Background -->
<rect width="620" height="500" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise IK)</text>
<!-- Column headers -->
<text x="100" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Initiator</text>
<text x="520" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Responder</text>
<!-- Lifelines -->
<line x1="100" y1="62" x2="100" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="520" y1="62" x2="520" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<!-- Step 1: Initiator prepares -->
<text x="20" y="88" fill="#8af8c8" font-size="10">generates sender_idx</text>
<text x="20" y="102" fill="#8af8c8" font-size="10">generates ephemeral keypair</text>
<!-- Arrow 1: msg1 (initiator -> responder) -->
<line x1="100" y1="120" x2="520" y2="120" stroke="#4a90d9" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="130" y="128" width="360" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x01|flags=0|len] | sender_idx | noise_msg1</text>
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 — 114 bytes</text>
<!-- Step 2: Responder processes (right-aligned to stay in bounds) -->
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1</text>
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator's static key</text>
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
<text x="600" y="234" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
<!-- Arrow 2: msg2 (responder -> initiator) -->
<!-- Draw line right-to-left so orient="auto" points the arrowhead left -->
<line x1="520" y1="256" x2="100" y2="256" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="130" y="264" width="360" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
<text x="310" y="280" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x02|flags=0|len] | sender_idx | receiver_idx | noise_msg2</text>
<text x="310" y="300" fill="#666" text-anchor="middle" font-size="9">phase 0x2 — 69 bytes</text>
<!-- Step 3: Initiator completes -->
<text x="20" y="324" fill="#8af8c8" font-size="10">validates msg2</text>
<text x="20" y="338" fill="#8af8c8" font-size="10">derives session keys</text>
<!-- Handshake complete separator -->
<line x1="30" y1="360" x2="230" y2="360" stroke="#d9904a" stroke-width="1.5"/>
<text x="310" y="364" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
<line x1="390" y1="360" x2="590" y2="360" stroke="#d9904a" stroke-width="1.5"/>
<!-- Arrow 3: first encrypted frame -->
<text x="20" y="392" fill="#777" font-size="10">first encrypted frame:</text>
<line x1="100" y1="406" x2="520" y2="406" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="414" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
<text x="310" y="430" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
<text x="310" y="450" fill="#666" text-anchor="middle" font-size="9">phase 0x0 — established frame</text>
<!-- Legend -->
<text x="310" y="478" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch exchange enables restart detection.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,63 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 430" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="430" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">LookupRequest (0x30) — 46 + 16n bytes</text>
<!-- Row 0 (03): msg_type + request_id starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x30</text>
<rect x="185" y="36" width="390" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="380" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">request_id</text>
<!-- Row 1 (48): request_id continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — unique random identifier</text>
<!-- Row 2 (924): target -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">924</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">target</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr being sought</text>
<!-- Row 3 (2540): origin -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2540</text>
<rect x="55" y="192" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">origin</text>
<text x="315" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — requester's NodeAddr</text>
<!-- Row 4 (4143): ttl + min_mtu -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">4143</text>
<rect x="55" y="244" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="120" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ttl</text>
<text x="120" y="286" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="244" width="390" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="380" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">min_mtu</text>
<text x="380" y="286" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 5 (4445): origin_coords_cnt -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">4445</text>
<rect x="55" y="296" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="185" y="326" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">origin_coords_cnt</text>
<!-- Row 6 (46): origin_coords -->
<text x="50" y="370" fill="#666" font-size="10" text-anchor="end">46</text>
<rect x="55" y="348" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="372" fill="#8af8f8" text-anchor="middle" font-size="11">origin_coords × n (16 bytes each — NodeAddr only)</text>
<!-- Total -->
<text x="310" y="414" fill="#777" font-size="10" text-anchor="middle">total: 46 + (n × 16) bytes, where n = origin depth + 1</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">LookupResponse (0x31) — 93 + 16n bytes</text>
<!-- Row 0 (03): msg_type + request_id starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x31</text>
<rect x="185" y="36" width="390" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="380" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">request_id</text>
<!-- Row 1 (48): request_id continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">48</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — echoes the request's ID</text>
<!-- Row 2 (924): target -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">924</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">target</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr that was found</text>
<!-- Row 3 (2528): path_mtu + target_coords_cnt -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2528</text>
<rect x="55" y="192" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="185" y="232" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<rect x="315" y="192" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="445" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">target_coords_cnt</text>
<text x="445" y="232" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 4 (29): target_coords -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">29</text>
<rect x="55" y="250" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="274" fill="#8af8f8" text-anchor="middle" font-size="11">target_coords × n (16 bytes each — NodeAddr only)</text>
<!-- Row 5: proof -->
<text x="50" y="316" fill="#666" font-size="10" text-anchor="end">+64</text>
<rect x="55" y="296" width="520" height="40" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">Schnorr proof (64 bytes)</text>
<!-- Total -->
<text x="310" y="358" fill="#777" font-size="10" text-anchor="middle">total: 93 + (n × 16) bytes · proof signs (request_id || target || target_coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="270" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">MtuExceeded (0x22) — 36 bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x22</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — destination being forwarded to</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Row 3 (3435): mtu -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="192" width="260" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">mtu</text>
<text x="185" y="232" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE — bottleneck MTU</text>
<!-- Note -->
<text x="310" y="264" fill="#777" font-size="10" text-anchor="middle">plaintext error signal (U flag set) · total on wire: 40 bytes (4 prefix + 36 payload)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+67
View File
@@ -0,0 +1,67 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 426" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="426" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 1 — phase 0x1 (114 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx (bytes 4-7) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise IK msg1 label -->
<text x="10" y="250" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 250)">Noise IK msg1 (106 bytes)</text>
<line x1="18" y1="140" x2="18" y2="348" stroke="#555" stroke-width="1"/>
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
<!-- Rows 2-3: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">840</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="170" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="222" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Rows 4-5: encrypted_static (bytes 41-89, 49 bytes) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">4189</text>
<rect x="55" y="244" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="274" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<rect x="55" y="296" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="326" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 6: encrypted_epoch (bytes 90-113, 24 bytes) -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">90113</text>
<rect x="55" y="348" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="372" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="390" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Total -->
<text x="310" y="418" fill="#777" font-size="10" text-anchor="middle">total: 114 bytes · payload_len = 110 · pattern: e, es, s, ss</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+65
View File
@@ -0,0 +1,65 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 2 — phase 0x2 (69 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1: sender_idx (bytes 4-7) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">47</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2: receiver_idx (bytes 8-11) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">811</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="315" y="182" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise IK msg2 bracket -->
<text x="10" y="270" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 270)">Noise IK msg2 (57 bytes)</text>
<line x1="18" y1="192" x2="18" y2="348" stroke="#555" stroke-width="1"/>
<line x1="18" y1="192" x2="23" y2="192" stroke="#555" stroke-width="1"/>
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
<!-- Rows 3-4: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">1244</text>
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="222" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="274" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Row 5: encrypted_epoch (bytes 45-68, 24 bytes) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">4568</text>
<rect x="55" y="296" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="338" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Total -->
<text x="310" y="366" fill="#777" font-size="10" text-anchor="middle">total: 69 bytes · payload_len = 65 · pattern: e, ee, se</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 296" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="296" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">PathBroken (0x21) — 36 + 16n bytes payload</text>
<!-- Row 0 (01): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="260" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="185" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="185" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">0x21</text>
<rect x="315" y="36" width="260" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="445" y="62" fill="#666" text-anchor="middle" font-size="12">flags (reserved)</text>
<!-- Row 1 (217): dest_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">217</text>
<rect x="55" y="88" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="128" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — unreachable NodeAddr</text>
<!-- Row 2 (1833): reporter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1833</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">reporter</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr of reporting router</text>
<!-- Row 3 (3435): last_coords_count -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="192" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="185" y="222" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">last_coords_count</text>
<!-- Row 4 (36): last_known_coords -->
<text x="50" y="264" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="248" width="520" height="40" fill="#1f1f3a" stroke="#d9904a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="272" fill="#f8c88a" text-anchor="middle" font-size="11">last_known_coords × n (16 bytes each — stale coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 114" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="114" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">PathMtuNotification (0x13) — 2 bytes body</text>
<!-- Row 0 (01): path_mtu -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">01</text>
<rect x="55" y="36" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="315" y="76" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE — minimum MTU observed along forward path</text>
<!-- Note -->
<text x="310" y="108" fill="#777" font-size="10" text-anchor="middle">body only — carried inside FSP encrypted message (prefix + inner header + AEAD tag)</text>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

+106
View File
@@ -0,0 +1,106 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 634" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="634" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">ReceiverReport (0x02) — 68 bytes</text>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
<!-- Row 1 (411): highest_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">highest_counter</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 2 (1219): cumulative_packets_recv -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 3 (2027): cumulative_bytes_recv -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</text>
<rect x="55" y="192" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 4 (2833): timestamp_echo + dwell_time -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">2833</text>
<rect x="55" y="244" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="185" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp_echo</text>
<text x="185" y="284" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="315" y="244" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="445" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dwell_time</text>
<text x="445" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE ms</text>
<!-- Row 5 (3439): max_burst_loss + mean_burst_loss + reserved -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">3439</text>
<rect x="55" y="296" width="173" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="141" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">max_burst</text>
<text x="141" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
<rect x="228" y="296" width="174" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">mean_burst</text>
<text x="315" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes u8.8</text>
<rect x="402" y="296" width="173" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="488" y="322" fill="#666" text-anchor="middle" font-size="10">reserved</text>
<!-- Row 6 (4047): jitter + ecn_ce_count -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">4047</text>
<rect x="55" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="185" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">jitter</text>
<text x="185" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE µs</text>
<rect x="315" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="445" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ecn_ce_count</text>
<text x="445" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 7 (4855): owd_trend + burst_loss_count -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">4855</text>
<rect x="55" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="185" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">owd_trend</text>
<text x="185" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes i32 LE µs/s</text>
<rect x="315" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="445" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">burst_loss_count</text>
<text x="445" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 8 (5663): cumulative_reorder_count + interval_packets_recv -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">5663</text>
<rect x="55" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">reorder_count</text>
<text x="185" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE cumulative</text>
<rect x="315" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="445" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_pkts_recv</text>
<text x="445" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 9 (6467): interval_bytes_recv -->
<text x="50" y="530" fill="#666" font-size="10" text-anchor="end">6467</text>
<rect x="55" y="504" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="526" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_bytes_recv</text>
<text x="185" y="544" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Total -->
<text x="310" y="586" fill="#777" font-size="10" text-anchor="middle">total: 68 bytes</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

+66
View File
@@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 478" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="478" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SenderReport (0x01) — 48 bytes</text>
<!-- Row 0 (03): msg_type + reserved -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
<!-- Row 1 (411): interval_start_counter -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">411</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_counter</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 2 (1219): interval_end_counter -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1219</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_counter</text>
<text x="315" y="180" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 3 (2027): interval_start_timestamp + interval_end_timestamp -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">2027</text>
<rect x="55" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_ts</text>
<text x="185" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="315" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_ts</text>
<text x="445" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 4 (2831): interval_bytes_sent -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">2831</text>
<rect x="55" y="244" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_bytes_sent</text>
<text x="315" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 5 (3239): cumulative_packets_sent -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">3239</text>
<rect x="55" y="296" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_sent</text>
<text x="315" y="336" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 6 (4047): cumulative_bytes_sent -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">4047</text>
<rect x="55" y="348" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_sent</text>
<text x="315" y="388" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Total -->
<text x="310" y="422" fill="#777" font-size="10" text-anchor="middle">total: 48 bytes</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+47
View File
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionAck (phase 0x2) — Noise XK msg2</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x2, flags=0, payload_len</text>
<!-- Row 1 (4): flags -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte — reserved</text>
<!-- Row 2 (5): src_coords (acknowledger) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">5</text>
<rect x="55" y="140" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="164" fill="#8af8f8" text-anchor="middle" font-size="11">src_coords_count(2) + src_coords(16 × n) — acknowledger</text>
<!-- Row 3: dest_coords (initiator) -->
<text x="50" y="206" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="186" width="520" height="40" fill="#1f1f3a" stroke="#b04ad9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="210" fill="#d8a0f8" text-anchor="middle" font-size="11">dest_coords_count(2) + dest_coords(16 × m) — initiator</text>
<!-- Row 4: handshake_len + handshake_payload -->
<text x="50" y="252" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="232" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="254" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">hs_len</text>
<text x="120" y="272" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg2 (57 bytes — ephemeral + epoch)</text>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~190 bytes (depth-dependent, carries both endpoints' coords)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionDatagram (0x00) — 36 + payload bytes</text>
<!-- Row 0 (03): msg_type + ttl + path_mtu -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x00</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ttl</text>
<text x="250" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="36" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="445" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">path_mtu</text>
<text x="445" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 1-4 (419): src_addr -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">419</text>
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">src_addr</text>
<text x="315" y="128" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — source NodeAddr</text>
<!-- Row 5-8 (2035): dest_addr -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">2035</text>
<rect x="55" y="140" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dest_addr</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">16 bytes — destination NodeAddr</text>
<!-- Row 9 (36): payload -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="198" width="520" height="40" fill="#1f1f3a" stroke="#444" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="222" fill="#666" text-anchor="middle" font-size="11">payload (variable — session-layer message)</text>
<!-- Notes -->
<text x="310" y="262" fill="#777" font-size="10" text-anchor="middle">fixed header: 36 bytes · payload opaque to transit nodes</text>
<text x="310" y="278" fill="#777" font-size="10" text-anchor="middle">ttl decremented and path_mtu min'd at each forwarding hop</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+35
View File
@@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 244" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="244" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionMsg3 (phase 0x3) — Noise XK msg3</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x3, flags=0, payload_len</text>
<!-- Row 1 (46): flags + handshake_len -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">46</text>
<rect x="55" y="88" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="120" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="120" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
<rect x="185" y="88" width="390" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="380" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_len</text>
<text x="380" y="128" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<!-- Row 2 (7): handshake_payload -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">7</text>
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg3 (73 bytes — encrypted static + encrypted epoch)</text>
<!-- Total -->
<text x="310" y="218" fill="#777" font-size="10" text-anchor="middle">~80 bytes · no coordinates (both endpoints already have them)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+47
View File
@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionSetup (phase 0x1) — Noise XK msg1</text>
<!-- Row 0 (03): FSP prefix -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="520" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="315" y="58" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">FSP common prefix</text>
<text x="315" y="76" fill="#8ab4f8" text-anchor="middle" font-size="10">ver=0, phase=0x1, flags=0, payload_len</text>
<!-- Row 1 (4): flags -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte — bit 0: REQUEST_ACK, bit 1: BIDIRECTIONAL</text>
<!-- Row 2 (5): src_coords -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">5</text>
<rect x="55" y="140" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="164" fill="#8af8f8" text-anchor="middle" font-size="11">src_coords_count(2) + src_coords(16 × n)</text>
<!-- Row 3: dest_coords -->
<text x="50" y="206" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="186" width="520" height="40" fill="#1f1f3a" stroke="#b04ad9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="210" fill="#d8a0f8" text-anchor="middle" font-size="11">dest_coords_count(2) + dest_coords(16 × m)</text>
<!-- Row 4: handshake_len + handshake_payload -->
<text x="50" y="252" fill="#666" font-size="10" text-anchor="end">...</text>
<rect x="55" y="232" width="130" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
<text x="120" y="254" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">hs_len</text>
<text x="120" y="272" fill="#f8d88a" text-anchor="middle" font-size="10">2 bytes LE</text>
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg1 (33 bytes — ephemeral key)</text>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~170 bytes (depth-dependent)</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

+59
View File
@@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
<!-- Background -->
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
<!-- Title -->
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">TreeAnnounce (0x10) — 100 + 32n bytes</text>
<!-- Row 0 (03): msg_type(1) + version(1) + sequence starts -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">03</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x10</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">version</text>
<text x="250" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">0x01</text>
<rect x="315" y="36" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="445" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
<!-- Row 1 (49): sequence continued -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">49</text>
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — monotonic counter</text>
<!-- Row 2 (1017): timestamp -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">1017</text>
<rect x="55" y="140" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE — Unix seconds</text>
<!-- Row 3 (1833): parent -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">1833</text>
<rect x="55" y="192" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">parent</text>
<text x="315" y="232" fill="#8af8f8" text-anchor="middle" font-size="10">16 bytes — NodeAddr (self = root)</text>
<!-- Row 4 (3435): ancestry_count -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">3435</text>
<rect x="55" y="250" width="260" height="40" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="185" y="274" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ancestry_count</text>
<!-- Row 5 (36): ancestry entries -->
<text x="50" y="316" fill="#666" font-size="10" text-anchor="end">36</text>
<rect x="55" y="296" width="520" height="40" fill="#1f1f3a" stroke="#d9904a" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="320" fill="#f8c88a" text-anchor="middle" font-size="11">AncestryEntry × n (32 bytes each)</text>
<!-- Signature -->
<text x="50" y="360" fill="#666" font-size="10" text-anchor="end">+64</text>
<rect x="55" y="342" width="520" height="26" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
<text x="315" y="360" fill="#e0e0e0" text-anchor="middle" font-size="10" font-weight="bold">Schnorr signature (64 bytes)</text>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+197
View File
@@ -0,0 +1,197 @@
# Nostr Event Reference
The Nostr-protocol surface FIPS uses for discovery and signaling. For
the design of the discovery runtime and the rationale behind these
event shapes, see
[../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md).
For operator activation recipes, see
[../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md).
FIPS uses three Nostr event kinds:
| Kind | Name | Encryption | Storage | Purpose |
| ---- | ---- | ---------- | ------- | ------- |
| 37195 | Overlay advert | None (signed only) | Replaceable | Publish reachable transport endpoints |
| 21059 | Traversal signaling | NIP-44 inside NIP-59 gift wrap | Ephemeral | Carry `TraversalOffer`/`TraversalAnswer` payloads |
| 10050 | NIP-17 inbox relay list | None (signed only) | Replaceable | Tell dialers where to publish offers |
All three are signed with the node's FIPS identity key (the same
secp256k1 keypair Nostr uses); there is no separate Nostr key.
## Kind 37195 — Overlay Advert
A parameterized replaceable event in the application-defined
replaceable range `3000039999` (the digits visually spell `FIPS`:
7=F, 1=I, 9=P, 5=S). Each node has a single in-place-updatable advert
under its identity.
### Tags
- `d` — fixed to the literal `fips-overlay-v1` (the application
identifier baked into the binary). Together with `pubkey`, this
identifies the unique replaceable event slot.
- `protocol` — the configured `node.discovery.nostr.app` value
(default `fips-overlay-v1`). Distinct from the `d` tag so the
application string can evolve without breaking the replaceable
event slot.
- `version` — protocol version string (currently `"1"`).
- `expiration` — NIP-40 expiration timestamp set to now +
`node.discovery.nostr.advert_ttl_secs` (default 3600 seconds).
Conforming relays stop serving the event after this time.
### Content
The event content is a JSON document shaped as `OverlayAdvert`:
```json
{
"identifier": "fips-overlay-v1",
"version": 1,
"endpoints": [
{"transport": "udp", "addr": "203.0.113.45:2121"},
{"transport": "tor", "addr": "xxxxx.onion:8443"},
{"transport": "udp", "addr": "nat"}
],
"signalRelays": ["wss://relay.damus.io", "wss://nos.lol"],
"stunServers": ["stun:stun.l.google.com:19302"]
}
```
Field semantics:
| Field | Type | Description |
| ----- | ---- | ----------- |
| `identifier` | string | Application namespace; must match the `d` tag. |
| `version` | integer | Advert schema version (currently 1). |
| `endpoints` | array | List of transport endpoints. Each is `{transport, addr}` where `transport` is `"udp"`, `"tcp"`, or `"tor"`, and `addr` is `"host:port"`, `".onion:port"`, or the literal `"nat"` (for UDP NAT-punch). |
| `signalRelays` | array? | Optional. Relays the publisher prefers for offer/answer signaling. Present only when at least one endpoint is `udp:nat`. |
| `stunServers` | array? | Optional. STUN servers the publisher uses for reflexive discovery. Present only when at least one endpoint is `udp:nat`. Informational — peers do not use these to choose their own STUN targets. |
### Signature scope
The Nostr event signature covers the standard Nostr event ID
(serialized `[0, pubkey, created_at, kind, tags, content]`), so the
content JSON, tags, kind, and timestamp are all bound to the signing
identity.
### Replacement and deletion
Because kind 37195 is replaceable, publishing a new advert replaces
the prior one in the same `(pubkey, d-tag)` slot. To withdraw an
advert without publishing a successor, the node publishes a NIP-9
kind 5 delete event referencing the prior advert.
## Kind 21059 — Traversal Signaling
An ephemeral event (kinds in the 2000029999 range are not stored by
conforming relays). Used to deliver gift-wrapped, NIP-44-encrypted
`TraversalOffer` and `TraversalAnswer` payloads between dialer and
responder during a UDP NAT hole-punch.
### Encryption envelope
The wire shape is the standard NIP-59 gift wrap:
1. **Rumor** — the unsigned `TraversalOffer`/`TraversalAnswer`
payload (JSON), authored by the actual sender's identity.
2. **Seal** — a kind 13 event whose content is the rumor
NIP-44-encrypted to the recipient's pubkey, signed by the sender.
3. **Gift wrap** — a kind 21059 event whose content is the seal
NIP-44-encrypted to the recipient under an ephemeral key, signed
by that ephemeral key. The outer `pubkey` of the kind 21059 event
is the ephemeral identity, not the sender's real identity.
Only the intended recipient can decrypt the wrap to recover the seal,
and only the recipient can decrypt the seal to recover the rumor.
### Wrapped payloads
The `TraversalOffer` carries:
- `type` — message-type tag.
- `sessionId` — unique identifier correlating offer and answer.
- `senderNpub` / `recipientNpub` — bech32-encoded pubkeys, repeated
inside the encrypted payload (the outer wrap pubkey is ephemeral).
- `issuedAt` / `expiresAt` — Unix-ms timestamps; `expiresAt` is
`issuedAt + signal_ttl_secs * 1000`.
- `nonce` — random per-offer value.
- `reflexiveAddress``{protocol, ip, port}` observed via STUN, or
`null` if STUN failed or returned no usable address.
- `localAddresses` — array of `{protocol, ip, port}` private
candidates, populated when `share_local_candidates` is enabled.
- `stunServer` — the STUN server actually used (informational).
The `TraversalAnswer` echoes `sessionId` and carries:
- `type`, `senderNpub`, `recipientNpub`, `issuedAt`, `expiresAt`,
`nonce` — same shape as the offer.
- `inReplyTo` — the offer's event id.
- `accepted` — boolean; false when the responder has no usable
addresses.
- `reflexiveAddress` and `localAddresses` — the responder's
candidates, in the same shape as the offer.
- `stunServer` — informational.
- `punch` — a `PunchHint { startAtMs, intervalMs, durationMs }`
telling both sides when to begin probing and how aggressively.
Absent on rejected offers.
- `reason` — optional rejection string when `accepted` is false.
- `offerReceivedAt` — optional responder wall-clock (Unix ms) at
the moment it received the offer; the initiator uses this to
derive a clock-skew estimate.
### Relay selection
Dialer publishes offers to the recipient's NIP-17 inbox relays (kind
10050) when available; otherwise to the local
`node.discovery.nostr.dm_relays` list. The responder publishes the
answer back through the same relay channel.
## Kind 10050 — NIP-17 Inbox Relay List
A standard NIP-17 event used by FIPS to advertise which relays this
node prefers for receiving direct-message-style signaling — for FIPS,
the gift-wrapped traversal offers (kind 21059).
This is **NIP-17** (`kind 10050`, inbox relays for DM delivery), not
NIP-65 (`kind 10002`, general read/write relay list). The two serve
different purposes:
- Kind 10002 (NIP-65) — general read/write relays for ordinary event
publication and subscription.
- Kind 10050 (NIP-17) — relays the recipient prefers for receiving
DM-shaped (NIP-59 wrapped) events.
FIPS publishes its own kind 10050 on startup so dialers can discover
where to send traversal offers. When dialing a peer, FIPS first
fetches the peer's kind 10050 from the peer's `advert_relays`; on
fetch failure it falls back to the local `dm_relays` list.
### Tags
Standard NIP-17 form: each relay is encoded as an `r` tag whose
single value is the relay URL.
```text
["r", "wss://relay.damus.io"]
["r", "wss://nos.lol"]
```
### Content
Empty per NIP-17.
## See also
- [../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md)
— discovery runtime design and the five activation scenarios
- [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md)
— operator recipes
- [../tutorials/resolve-peers-via-nostr.md](../tutorials/resolve-peers-via-nostr.md),
[../tutorials/advertise-your-node.md](../tutorials/advertise-your-node.md),
[../tutorials/open-discovery.md](../tutorials/open-discovery.md)
— hand-held tutorial walkthroughs of the three capabilities
- [../design/port-advertisement-and-nat-traversal.md](../design/port-advertisement-and-nat-traversal.md)
— generic protocol reference (event tags, NIP usage, on-the-wire
offer/answer schema), with FIPS values as worked examples
- [security.md](security.md) — how the FIPS identity key signs both
adverts and Noise handshakes
+235
View File
@@ -0,0 +1,235 @@
# Security Reference
Consolidated security reference covering the nftables baseline, peer
ACL file format, cryptographic primitives, rekey defaults, replay
window, filesystem permissions, threat-resistance matrix, and default
network exposures per transport. For the threat-model design and
rationale, see [../design/fips-security.md](../design/fips-security.md).
For the operator activation steps and drop-in recipes, see
[../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md).
## nftables Baseline
The shipped baseline is `/etc/fips/fips.nft`. It defines a single
nftables table `inet fips` with one chain hooked at `input`, structured
as follows:
| Step | Rule | Effect |
| ---- | ---- | ------ |
| 1 | `iifname != "fips0" return` | Match only traffic arriving on `fips0`; everything else short-circuits. |
| 2 | `ct state established,related accept` | Allow conntrack replies and related ICMPv6 errors. |
| 3 | `icmpv6 type echo-request accept` | Allow IPv6 echo (ping6 reachability). |
| 4 | `include "/etc/fips/fips.d/*.nft"` | Splice in operator drop-ins (empty matches nothing). |
| 5 | `counter drop` | Default-deny everything else; counter increments on every drop. |
Outbound from `fips0` is unrestricted. The baseline is a documented
dpkg conffile — operator edits to `/etc/fips/fips.nft` are preserved
across upgrades.
The systemd unit is `fips-firewall.service` (oneshot). It is **not**
enabled by default; activation is an explicit operator gesture
documented in
[../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md).
## Drop-In File Format
Operator extensions live under `/etc/fips/fips.d/` with the `.nft`
suffix. Each file is included inline into the `inbound` chain at the
marked point and may contain any nftables rule lines valid in that
context.
Naming convention: `<purpose>-from-<source>.nft` keeps drop-ins easy
to scan. Examples shipped in the design discussion:
- `ssh-from-bastion.nft` — accept TCP/22 from a single mesh-node address
- `http-from-cluster.nft` — accept TCP/80 from a `/64` mesh-address prefix
- `dns-public.nft` — accept UDP/53 and TCP/53 from any mesh node
- `git-from-trusted.nft` — accept TCP/9418 from a set of mesh-node addresses
After editing, reload via
`sudo systemctl reload-or-restart fips-firewall.service` (or
equivalently `sudo nft -f /etc/fips/fips.nft` since the file is
idempotent).
## Cryptographic Primitives
| Component | Choice | Where Used |
| --------- | ------ | ---------- |
| Curve | secp256k1 | FMP IK, FSP XK, Schnorr signatures |
| Diffie-Hellman | ECDH on secp256k1 (x-only normalized) | Noise IK, Noise XK |
| AEAD | ChaCha20-Poly1305 | FMP link encryption, FSP session encryption |
| Hash | SHA-256 | NodeAddr derivation, Noise transcript |
| Key derivation | HKDF-SHA256 | Noise key schedule |
| Signatures | secp256k1 Schnorr | TreeAnnounce, LookupResponse proof, Nostr adverts |
| Noise pattern (link) | `Noise_IK_secp256k1_ChaChaPoly_SHA256` | FMP link layer (IK with epoch payload) |
| Noise pattern (session) | `Noise_XK_secp256k1_ChaChaPoly_SHA256` | FSP session layer (XK with epoch payload) |
These choices align with the Nostr cryptographic stack
(secp256k1 + ChaCha20-Poly1305 + SHA-256) and the NIP-44 encrypted
messaging standard.
## Rekey Defaults
Both link-layer and session-layer Noise sessions rekey under one of
two triggers, configurable under `node.rekey.*`:
| Parameter | Default | Description |
| --------- | ------- | ----------- |
| `enabled` | `true` | Master switch. |
| `after_secs` | `120` | Time-based rekey threshold. |
| `after_messages` | `65536` | Message-count rekey threshold. |
In addition to the configurable triggers, the daemon retains the old
session keys for a fixed **10-second drain window** after each
cutover (compile-time constant `DRAIN_WINDOW_SECS` in
`src/node/handlers/rekey.rs`). Rekey rotates the Noise key schedule
and the session indices; old session keys are kept in
`previous_session` for the drain window so in-flight packets
encrypted under the old keys still decrypt.
## Replay Window
Both layers use explicit per-packet counters with a sliding bitmap
window for replay protection. The bitmap is **2048 entries** at both
layers — large enough to accommodate UDP reordering and packet loss
without false-positive replay rejection. Counters older than the
window are rejected. The same `ReplayWindow` and
`decrypt_with_replay_check()` implementation is used at both the FMP
and FSP layers.
## Peer ACL
Mesh-level ACL files at `/etc/fips/peers.allow` and
`/etc/fips/peers.deny` give the operator allowlist/blocklist control
over which npubs may complete the FMP Noise IK link handshake.
File format:
- One entry per line. An entry is either a bech32 `npub1...`,
an alias defined in `/etc/fips/hosts`, or the literal `ALL`
wildcard (case-insensitive).
- Lines beginning with `#` are comments.
- Blank lines are ignored.
Evaluation order (first match wins, default-allow on no match):
1. `peers.allow` — if the peer matches an entry here (or `ALL` is
in `peers.allow`), the handshake is admitted, regardless of any
`peers.deny` entry.
2. `peers.deny` — if the peer matches an entry here (or `ALL` is
in `peers.deny`), the handshake is refused.
3. Otherwise the peer is admitted.
`peers.allow` is **not** an exclusive gate on its own: an unlisted
peer falls through to step 3 and is admitted unless it appears in
`peers.deny`. To turn `peers.allow` into a strict allowlist, place
`ALL` in `peers.deny` so every unlisted peer is rejected at step 2.
The `ALL` wildcard makes the operator's posture explicit:
- `ALL` in `peers.allow` admits every peer (same effect as the
default-allow behavior, but documented in the file).
- `ALL` in `peers.deny` blocks every peer except those listed in
`peers.allow` — the "allowlist-strict" posture.
In practice this collapses to a few common postures:
- **Default-allow with denylist**: leave `peers.allow` empty;
populate `peers.deny`. All npubs may peer except those listed.
- **Allowlist-strict**: populate `peers.allow` and put `ALL`
in `peers.deny`. Only the listed npubs may peer; everyone else
is rejected at step 2.
A populated `peers.allow` with an empty `peers.deny` is not a
strict allowlist — it is equivalent to default-allow plus an
explicit "always-admit" set. The strict variant requires `ALL`
in `peers.deny`.
Aliases are resolved through `/etc/fips/hosts` at file-load
time. If `peers.allow` lists `core-vm` and `/etc/fips/hosts`
maps `core-vm` to a specific npub, that npub is admitted. If
`core-vm` is later remapped to a different npub, the ACL
re-resolves on the next mtime change. Operators should be aware
that ACL semantics follow the `hosts`-file aliasing, not just
the literal npubs visible in the file.
Both files are reloaded automatically when their mtime changes
— no daemon restart or signal is needed. ACL evaluation runs
after msg1 decryption but before any further peer-state
mutation; rate-limited msg1s never reach the ACL.
## Filesystem Permissions
| Path | Owner | Mode | Purpose |
| ---- | ----- | ---- | ------- |
| `/etc/fips/fips.key` | root:root | `0600` | Persistent identity private key (sensitive). |
| `/etc/fips/fips.pub` | root:root | `0644` | Public key (npub). |
| `/etc/fips/fips.yaml` | root:root | `0644` | Daemon configuration (dpkg conffile). |
| `/etc/fips/fips.nft` | root:root | `0644` | nftables baseline (dpkg conffile). |
| `/etc/fips/fips.d/` | root:root | `0755` | Operator drop-in directory. |
| `/etc/fips/hosts` | root:root | `0644` | Optional hostname → npub map (dpkg conffile). |
| `/etc/fips/peers.allow` | root:root | `0644` | Optional peer allowlist. |
| `/etc/fips/peers.deny` | root:root | `0644` | Optional peer denylist. |
| `/run/fips/control.sock` | root:fips | `0770` | Control socket (members of `fips` group can use `fipsctl`). |
| `/run/fips/` | root:fips | `0750` | Control socket parent directory. |
Adding a user to the `fips` group grants `fipsctl` access without
requiring root. The daemon `chown`s the control socket and its parent
directory at bind time.
## Threat-Resistance Matrix
The link layer's threat-resistance matrix is consolidated here from
the FMP design document:
| Threat | Mitigation |
| ------ | ---------- |
| Connection exhaustion | Token-bucket rate limit + connection count limit |
| CPU exhaustion (msg1 flood) | Rate limit before crypto operations |
| Replay attacks | Counter-based nonces with sliding window (2048 entries) |
| State confusion | Strict handshake state machine validation |
| Spoofed encrypted packets | Index lookup + AEAD verification |
| Spoofed msg2 | Index lookup + Noise ephemeral key binding |
| Address spoofing | Cryptographic authority, not address-based |
| Session correlation | Index rotation on rekey |
| Inbound exposure on `fips0` | Default-deny nftables baseline (operator opt-in) |
| Sybil identities | Discretionary peering + handshake rate limiting + optional peer ACL |
| Eclipse attack | Diverse peering across independent operators and transports |
| Unauthorized peer admission | Optional `peers.allow` allowlist consulted before handshake |
See [../design/fips-mesh-layer.md](../design/fips-mesh-layer.md) for
the unauthenticated-attack-surface analysis (only handshake msg1 is
reachable by unauthenticated parties), and
[../design/fips-mesh-operation.md](../design/fips-mesh-operation.md#privacy-considerations)
for the metadata-privacy model and the rejection of onion routing.
## Default Network Exposures by Transport
| Transport | Default Inbound | Default Bind | Opt-in |
| --------- | --------------- | ------------ | ------ |
| UDP | None until `bind_addr` set | `0.0.0.0:2121` typical | Operator sets `transports.udp.bind_addr` |
| TCP | None until `bind_addr` set | None — outbound-only without bind | Operator sets `transports.tcp.bind_addr` |
| Ethernet | Listens on configured interface (raw `AF_PACKET`) | EtherType 0x2121 on selected interface | Per-flag `discovery`, `announce`, `auto_connect`, `accept_connections` |
| Tor | None until `directory_service` configured | `127.0.0.1:8443` (loopback only) | Operator sets `transports.tor.directory_service` and configures `HiddenServiceDir` in `torrc` |
| BLE | Off by default | n/a | Operator enables `transports.ble.*` |
| Nostr discovery | Off by default | n/a (relay client, not a listener) | Operator sets `node.discovery.nostr.enabled: true` |
The mesh-layer `fips0` interface is reachable from any mesh node that
can route to you, not only direct peers — your direct peers forward
traffic from any reachable mesh node onto your `fips0`. The
default-deny nftables baseline (operator opt-in) is the recommended
way to restrict inbound traffic on `fips0`. See
[../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md).
## See also
- [../design/fips-security.md](../design/fips-security.md) — threat
model and design rationale for the `fips0` baseline
- [../design/fips-mesh-layer.md](../design/fips-mesh-layer.md) — FMP
link encryption, replay protection, rate limiting
- [../design/fips-session-layer.md](../design/fips-session-layer.md)
— FSP end-to-end encryption, Noise XK, replay window
- [../how-to/enable-mesh-firewall.md](../how-to/enable-mesh-firewall.md)
— operator activation and drop-in recipes
- [configuration.md](configuration.md) — full `node.rekey.*`,
`node.rate_limit.*` parameter tables
+92
View File
@@ -0,0 +1,92 @@
# Transport Statistics Reference
Per-transport statistics counter inventories. Counters are exposed
through the daemon control socket (`fipsctl show transports`) and the
`fipstop` operator UI. For the transport-layer design (services
provided to FMP, transport categories, the trait surface, connection
model), see
[../design/fips-transport-layer.md](../design/fips-transport-layer.md).
All transports report counters via `fipsctl show transports`; the
tables below are source-extracted from each transport's `stats.rs`
module.
## UDP
| Counter | Description |
| ------- | ----------- |
| `packets_sent` / `bytes_sent` | Successful sends |
| `packets_recv` / `bytes_recv` | Successful receives |
| `send_errors` / `recv_errors` | Send/receive failures |
| `mtu_exceeded` | Packets rejected for MTU violation |
| `kernel_drops` | Kernel `SO_RXQ_OVFL` drop count (feeds ECN congestion detection) |
## TCP
| Counter | Description |
| ------- | ----------- |
| `packets_sent` / `bytes_sent` | Successful sends |
| `packets_recv` / `bytes_recv` | Successful receives |
| `send_errors` / `recv_errors` | Send/receive failures |
| `mtu_exceeded` | Packets rejected for MTU violation |
| `connections_established` | Successful outbound connections |
| `connections_accepted` | Accepted inbound connections |
| `connections_rejected` | Rejected inbound connections (limit exceeded) |
| `connect_timeouts` | Connection timeout count |
| `connect_refused` | Connection refused count |
## Ethernet
| Counter | Description |
| ------- | ----------- |
| `frames_sent` / `frames_recv` | Successful frame send/receive |
| `bytes_sent` / `bytes_recv` | Byte counters |
| `send_errors` / `recv_errors` | Send/receive failures |
| `beacons_sent` / `beacons_recv` | Peer-discovery beacon traffic |
| `frames_too_short` | Frames below minimum length, dropped |
| `frames_too_long` | Frames above transport MTU, dropped |
## Tor
| Counter | Description |
| ------- | ----------- |
| `packets_sent` / `bytes_sent` | Successful sends |
| `packets_recv` / `bytes_recv` | Successful receives |
| `send_errors` / `recv_errors` | Send/receive failures |
| `connections_established` | Successful SOCKS5 connections |
| `connect_timeouts` | Connection timeout count |
| `connect_refused` | Connection refused count |
| `socks5_errors` | SOCKS5 protocol errors |
| `mtu_exceeded` | Packets rejected for MTU violation |
| `connections_accepted` | Accepted inbound connections via onion service |
| `connections_rejected` | Rejected inbound connections (limit exceeded) |
| `control_errors` | Tor control port errors |
## Bluetooth
| Counter | Description |
| ------- | ----------- |
| `packets_sent` / `bytes_sent` | Successful L2CAP CoC sends |
| `packets_recv` / `bytes_recv` | Successful L2CAP CoC receives |
| `send_errors` / `recv_errors` | Send/receive failures |
| `mtu_exceeded` | Packets rejected for MTU violation |
| `connections_established` | Successful outbound L2CAP connections |
| `connections_accepted` | Accepted inbound L2CAP connections |
| `connections_rejected` | Rejected inbound (limit exceeded) |
| `connect_timeouts` | Connection timeout count |
| `pool_evictions` | Connection-pool entries evicted |
| `advertisements_sent` | BLE advertisements emitted |
| `scan_results` | BLE scan results observed |
## See also
- [../design/fips-transport-layer.md](../design/fips-transport-layer.md)
— transport-layer design, trait surface, per-transport sections
- [configuration.md](configuration.md) — `transports.*` configuration
blocks
- [../how-to/tune-udp-buffers.md](../how-to/tune-udp-buffers.md) —
host-side `net.core.rmem_max` / `net.core.wmem_max` setup for UDP
- [../how-to/deploy-tor-onion.md](../how-to/deploy-tor-onion.md) —
Tor `directory` mode operator setup
- [../how-to/set-up-bluetooth-peer.md](../how-to/set-up-bluetooth-peer.md)
— Linux BLE peer config
+938
View File
@@ -0,0 +1,938 @@
# FIPS Wire Formats
This document is the comprehensive wire format reference for all three
protocol layers. It covers transport framing, link-layer message formats,
and session-layer message formats, with an encapsulation walkthrough showing
how application data is wrapped through each layer.
## FMP Message Type Catalog
The FMP link layer defines the following message types, dispatched by the
`msg_type` byte in the encrypted inner header:
| Type | Name | Forwarding |
| ---- | ---- | ---------- |
| 0x00 | SessionDatagram | Routed hop-by-hop toward the destination |
| 0x01 | SenderReport | Peer-to-peer (MMP, link-layer instance) |
| 0x02 | ReceiverReport | Peer-to-peer (MMP, link-layer instance) |
| 0x10 | TreeAnnounce | Peer-to-peer (spanning-tree gossip) |
| 0x20 | FilterAnnounce | Peer-to-peer (bloom-filter gossip) |
| 0x30 | LookupRequest | Forwarded — bloom-guided through tree peers |
| 0x31 | LookupResponse | Forwarded — reverse-path via `recent_requests` |
| 0x50 | Disconnect | Peer-to-peer (orderly link teardown) |
| 0x51 | Heartbeat | Peer-to-peer (link liveness) |
Handshake messages travel before encryption is established and are identified
by the FMP common-prefix `phase` field rather than a `msg_type` byte
(phase 0x1 = Noise IK msg1, phase 0x2 = Noise IK msg2).
## Packet Type Summary
A higher-level summary that includes typical sizes and forwarding category:
| Message | Typical Size | When | Forwarded? |
| ------- | ------------ | ---- | ---------- |
| TreeAnnounce | Variable (depth-dependent) | Topology changes | No (peer-to-peer) |
| FilterAnnounce | ~1 KB | Topology changes | No (peer-to-peer) |
| LookupRequest | ~300 bytes | First contact, recovery | Yes (bloom-guided tree) |
| LookupResponse | ~400 bytes | Response to discovery | Yes (reverse-path) |
| SessionDatagram + SessionSetup | ~232402 bytes | Session establishment | Yes (routed) |
| SessionDatagram + SessionAck | ~170 bytes | Session confirmation | Yes (routed) |
| SessionDatagram + Data (minimal) | 77 bytes + IPv6 payload | Bulk IPv6 traffic (compressed) | Yes (routed) |
| SessionDatagram + Data (with CP) | 77 + coords + IPv6 payload | Warmup/recovery (compressed) | Yes (routed) |
| SessionDatagram + CoordsRequired | 70 bytes | Cache miss error | Yes (routed) |
| SessionDatagram + PathBroken | 70+ bytes | Dead-end error | Yes (routed) |
| Disconnect | 2 bytes | Link teardown | No (peer-to-peer) |
## Encoding Rules
- All multi-byte integers are **little-endian** (LE)
- NodeAddr is **16 bytes** — truncated SHA-256 hash of public key
- Signatures are **64 bytes** — secp256k1 Schnorr
- Variable-length arrays use a **2-byte u16 LE count prefix** followed by
that many items
- Public keys are **33 bytes** — compressed secp256k1 (02/03 prefix + 32
bytes)
## Transport Framing
Datagram-oriented transports (UDP, raw Ethernet, radio) preserve natural
packet boundaries and require no additional framing. Stream-oriented
transports (TCP, Tor) must delineate FIPS packets within the byte
stream; the common prefix `payload_len` field provides this framing
directly. TCP and Tor share a common stream reader (`tcp/stream.rs`)
that implements this framing.
**Ethernet data frame header.** The Ethernet transport prepends a 3-byte
header before the FMP payload on data frames: a 1-byte frame type
(`0x00`) followed by a 2-byte little-endian payload length. The length
field allows the receiver to trim Ethernet minimum-frame padding that
would otherwise corrupt AEAD verification. Beacon frames (`0x01`) have
no length field (fixed 34-byte payload). These bytes are consumed by the
transport layer and are not visible to FMP. The effective MTU for FMP is
the interface MTU minus three bytes (typically 1497).
## Link-Layer Formats
All FMP packets begin with a **4-byte common prefix** that identifies the
protocol version, session lifecycle phase, per-packet flags, and payload
length.
### Common Prefix (4 bytes)
![Common Prefix](diagrams/common-prefix.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
| version | 4 bits (high) | Protocol version. Currently 0x0 |
| phase | 4 bits (low) | Session lifecycle phase (see table) |
| flags | 1 byte | Per-packet signal flags (zero during handshake) |
| payload_len | 2 bytes LE | Length of payload after phase-specific header, excluding AEAD tag |
### Phase Table
| Phase | Type | Description |
| ----- | ---- | ----------- |
| 0x0 | Established frame | Post-handshake encrypted traffic |
| 0x1 | Noise IK msg1 | Handshake initiation |
| 0x2 | Noise IK msg2 | Handshake response |
### Flags (Established Phase Only)
| Bit | Name | Description |
| --- | ---- | ----------- |
| 0 | K (key epoch) | Selects active key during rekeying |
| 1 | CE | Congestion Experienced echo |
| 2 | SP (spin bit) | RTT measurement |
| 3-7 | — | Reserved (must be zero) |
Flags must be zero in handshake packets (phase 0x1 and 0x2).
### Established Frame (phase 0x0)
All post-handshake traffic between authenticated peers. Contains one
encrypted link-layer message.
**Outer header** (16 bytes, used as AEAD AAD):
![Established frame outer header](diagrams/established-outer-header.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=0, flags, payload_len |
| receiver_idx | 4 bytes LE | Session index for O(1) lookup |
| counter | 8 bytes LE | Monotonic nonce, used as AEAD nonce and for replay detection |
The entire 16-byte header is authenticated as Associated Data (AAD) in the
ChaCha20-Poly1305 AEAD construction.
**Encrypted inner header** (5 bytes, first bytes of plaintext):
![Established frame inner header](diagrams/established-inner-header.svg)
| Field | Size | Description |
| ----- | ---- | ----------- |
| timestamp | 4 bytes LE | Session-relative milliseconds (u32) |
| msg_type | 1 byte | Link-layer message type |
After decryption, the plaintext begins with the 4-byte timestamp followed by
the 1-byte message type and message-specific fields.
**Complete encrypted frame**:
![Complete encrypted frame](diagrams/established-complete-frame.svg)
### Message Type Table
| Type | Message | Description |
| ---- | ------- | ----------- |
| 0x00 | SessionDatagram | Encapsulated session-layer payload for forwarding |
| 0x01 | SenderReport | MMP sender-side metrics report (48 bytes) |
| 0x02 | ReceiverReport | MMP receiver-side metrics report (68 bytes) |
| 0x10 | TreeAnnounce | Spanning tree state announcement |
| 0x20 | FilterAnnounce | Bloom filter reachability update |
| 0x30 | LookupRequest | Coordinate discovery request |
| 0x31 | LookupResponse | Coordinate discovery response |
| 0x50 | Disconnect | Orderly link teardown |
| 0x51 | Heartbeat | Link liveness probe |
### Noise IK Message 1 (phase 0x1)
Handshake initiation from connecting party.
![Noise IK message 1](diagrams/noise-ik-msg1.svg)
Common prefix: ver=0, phase=0x1, flags=0, payload_len=110 (4 + 106).
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=1, flags=0, payload_len |
| sender_idx | 4 bytes LE | Initiator's session index (becomes receiver's `receiver_idx`) |
| noise_msg1 | 106 bytes | Noise IK first message |
**Noise msg1 breakdown** (106 bytes):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | ephemeral_pubkey | 33 bytes | Initiator's ephemeral key (compressed secp256k1) |
| 33 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
| 82 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
Noise pattern: `-> e, es, s, ss` with epoch payload
### Noise IK Message 2 (phase 0x2)
Handshake response from responder.
![Noise IK message 2](diagrams/noise-ik-msg2.svg)
Common prefix: ver=0, phase=0x2, flags=0, payload_len=65 (4 + 4 + 57).
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=2, flags=0, payload_len |
| sender_idx | 4 bytes LE | Responder's session index |
| receiver_idx | 4 bytes LE | Echo of initiator's sender_idx from msg1 |
| noise_msg2 | 57 bytes | Noise IK second message |
**Noise msg2 breakdown** (57 bytes):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | ephemeral_pubkey | 33 bytes | Responder's ephemeral key (compressed secp256k1) |
| 33 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
Noise pattern: `<- e, ee, se` with epoch payload
After msg2, both parties derive identical symmetric session keys. The
encrypted epoch in msg1 and msg2 enables peer restart detection — if a
peer's epoch changes, the other side knows it restarted and must
re-establish the link.
### Index Semantics
Each party in a link session maintains two indices:
| Index | Chosen By | Used By | Purpose |
| ----- | --------- | ------- | ------- |
| our_index | Us | Them | They include this as `receiver_idx` in packets to us |
| their_index | Them | Us | We include this as `receiver_idx` in packets to them |
### Handshake Flow
![Handshake flow](diagrams/handshake-flow.svg)
## Link-Layer Message Types
These messages are carried as plaintext inside encrypted frames (phase 0x0).
After decryption of the AEAD ciphertext, the plaintext begins with a 4-byte
session-relative timestamp followed by the 1-byte message type and
message-specific fields.
### TreeAnnounce (0x10)
Spanning tree state announcement, exchanged between direct peers only.
![TreeAnnounce](diagrams/tree-announce.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x10 |
| 1 | version | 1 byte | 0x01 (v1) |
| 2 | sequence | 8 bytes LE | Monotonic counter, increments on parent change |
| 10 | timestamp | 8 bytes LE | Unix seconds |
| 18 | parent | 16 bytes | NodeAddr of selected parent (self = root) |
| 34 | ancestry_count | 2 bytes LE | Number of AncestryEntry records |
| 36 | ancestry | 32 x n bytes | AncestryEntry array (self -> root) |
| 36 + 32n | signature | 64 bytes | Schnorr signature over entire message |
**AncestryEntry** (32 bytes):
![AncestryEntry](diagrams/ancestry-entry.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | node_addr | 16 bytes | Node's routing identifier |
| 16 | sequence | 8 bytes LE | Node's sequence number |
| 24 | timestamp | 8 bytes LE | Node's Unix timestamp |
**Size**: `100 + (n x 32)` bytes, where n = `ancestry_count` (depth + 1,
includes self)
| Tree Depth | Payload | With Link Overhead |
| ---------- | ------- | ------------------ |
| 0 (root) | 132 bytes | 169 bytes |
| 3 | 228 bytes | 265 bytes |
| 5 | 292 bytes | 329 bytes |
| 10 | 452 bytes | 489 bytes |
### FilterAnnounce (0x20)
Bloom filter reachability update, exchanged between direct peers only.
![FilterAnnounce](diagrams/filter-announce.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x20 |
| 1 | sequence | 8 bytes LE | Monotonic counter for freshness |
| 9 | hash_count | 1 byte | Number of hash functions (5 in v1) |
| 10 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
| 11 | filter_bits | variable | Bloom filter bit array |
**Size class table**:
| size_class | Bytes | Bits | Status |
| ---------- | ----- | ---- | ------ |
| 0 | 512 | 4,096 | Reserved |
| 1 | 1,024 | 8,192 | **v1 (MUST use)** |
| 2 | 2,048 | 16,384 | Reserved |
| 3 | 4,096 | 32,768 | Reserved |
**v1 payload**: 1,035 bytes (11 header + 1,024 filter).
With link overhead: 1,072 bytes.
### LookupRequest (0x30)
Coordinate discovery request, routed through the spanning tree via
bloom-filter-guided forwarding. Each transit node forwards only to tree
peers (parent + children) whose bloom filter contains the target.
Request_id dedup in recent_requests handles edge cases from tree
restructuring.
![LookupRequest](diagrams/lookup-request.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x30 |
| 1 | request_id | 8 bytes LE | Unique random identifier |
| 9 | target | 16 bytes | NodeAddr being sought |
| 25 | origin | 16 bytes | Requester's NodeAddr |
| 41 | ttl | 1 byte | Remaining hops (default 64) |
| 42 | min_mtu | 2 bytes LE | Minimum transport MTU the origin requires (0 = no requirement) |
| 44 | origin_coords_cnt | 2 bytes LE | Number of coordinate entries |
| 46 | origin_coords | 16 x n bytes | Requester's ancestry (NodeAddr only) |
**Size**: `46 + (n x 16)` bytes, where n = origin depth + 1
| Origin Depth | Payload |
| ------------ | ------- |
| 3 | 110 bytes |
| 5 | 142 bytes |
| 10 | 222 bytes |
### LookupResponse (0x31)
Coordinate discovery response, reverse-path routed back to the
requester via the transit nodes that forwarded the request.
![LookupResponse](diagrams/lookup-response.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x31 |
| 1 | request_id | 8 bytes LE | Echoes the request's ID |
| 9 | target | 16 bytes | NodeAddr that was found |
| 25 | path_mtu | 2 bytes LE | Minimum MTU along response path (transit-annotated) |
| 27 | target_coords_cnt | 2 bytes LE | Number of coordinate entries |
| 29 | target_coords | 16 x n bytes | Target's ancestry (NodeAddr only) |
| 29 + 16n | proof | 64 bytes | Schnorr signature over `(request_id \|\| target \|\| target_coords)` |
**Size**: `93 + (n x 16)` bytes
| Target Depth | Payload |
| ------------ | ------- |
| 3 | 141 bytes |
| 5 | 173 bytes |
| 10 | 253 bytes |
The `path_mtu` field is initialized to `u16::MAX` by the target and each
transit hop applies `min(path_mtu, outgoing_link_mtu)`, giving the
originator an MTU estimate for the discovered path.
**Proof coverage**: Signs `(request_id || target || target_coords)`
`path_mtu` is excluded from the proof because it is a transit annotation
modified at each hop. Coordinates are included because proof verification
at the source confirms the target actually holds the claimed tree position.
The source verifies the proof upon receipt.
### SessionDatagram (0x00)
Encapsulated session-layer payload for multi-hop forwarding.
![SessionDatagram](diagrams/session-datagram.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x00 |
| 1 | ttl | 1 byte | Remaining hops, decremented each hop |
| 2 | path_mtu | 2 bytes LE | Path MTU, min'd at each forwarding hop |
| 4 | src_addr | 16 bytes | Source NodeAddr |
| 20 | dest_addr | 16 bytes | Destination NodeAddr |
| 36 | payload | variable | Session-layer message |
**Fixed header**: 36 bytes (`SESSION_DATAGRAM_HEADER_SIZE`)
The `path_mtu` field is initialized to `u16::MAX` by the sender and each
forwarding hop applies `min(path_mtu, outgoing_link_mtu)`, giving the
receiver an estimate of the minimum MTU along the path.
The payload is opaque to transit nodes — session-layer encrypted
independently of link encryption.
### Disconnect (0x50)
Orderly link teardown with reason code.
![Disconnect](diagrams/disconnect.svg)
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x50 |
| 1 | reason | 1 byte | Disconnect reason code |
**Reason codes**:
| Code | Name | Description |
| ---- | ---- | ----------- |
| 0x00 | Shutdown | Normal operator-requested stop |
| 0x01 | Restart | Restarting, may reconnect soon |
| 0x02 | ProtocolError | Protocol error encountered |
| 0x03 | TransportFailure | Transport failure |
| 0x04 | ResourceExhaustion | Memory or connection limit |
| 0x05 | SecurityViolation | Authentication or policy violation |
| 0x06 | ConfigurationChange | Peer removed from configuration |
| 0x07 | Timeout | Heartbeat liveness timeout |
| 0xFF | Other | Unspecified reason |
### SenderReport (0x01)
Sent by the frame sender to provide interval-based transmission statistics.
![SenderReport](diagrams/sender-report.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x01` |
| 1 | reserved | 3 | Zero |
| 4 | interval_start_counter | 8 | u64 LE — first counter in this interval |
| 12 | interval_end_counter | 8 | u64 LE — last counter in this interval |
| 20 | interval_start_timestamp | 4 | u32 LE — timestamp at interval start |
| 24 | interval_end_timestamp | 4 | u32 LE — timestamp at interval end |
| 28 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval |
| 32 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link |
| 40 | cumulative_bytes_sent | 8 | u64 LE — total bytes sent on this link |
**Total: 48 bytes.**
### ReceiverReport (0x02)
Sent by the frame receiver to provide loss, jitter, and timing feedback.
![ReceiverReport](diagrams/receiver-report.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | msg_type | 1 | `0x02` |
| 1 | reserved | 3 | Zero |
| 4 | highest_counter | 8 | u64 LE — highest counter value received |
| 12 | cumulative_packets_recv | 8 | u64 LE — total packets received |
| 20 | cumulative_bytes_recv | 8 | u64 LE — total bytes received |
| 28 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT |
| 32 | dwell_time | 2 | u16 LE — time between receive and echo (ms) |
| 34 | max_burst_loss | 2 | u16 LE — largest loss burst in interval |
| 36 | mean_burst_loss | 2 | u16 LE — mean burst length (u8.8 fixed-point) |
| 38 | reserved | 2 | Zero |
| 40 | jitter | 4 | u32 LE — interarrival jitter (microseconds) |
| 44 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets |
| 48 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) |
| 52 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval |
| 56 | cumulative_reorder_count | 4 | u32 LE — total reordered packets |
| 60 | interval_packets_recv | 4 | u32 LE — packets received in interval |
| 64 | interval_bytes_recv | 4 | u32 LE — bytes received in interval |
**Total: 68 bytes.**
## Session-Layer Message Formats
Session-layer messages are carried as the payload of a SessionDatagram (0x00).
All FSP messages begin with a **4-byte common prefix** that identifies the
protocol version, session lifecycle phase, per-packet flags, and payload length.
### FSP Common Prefix (4 bytes)
| Field | Size | Description |
| ----- | ---- | ----------- |
| version | 4 bits (high) | Protocol version. Currently 0x0 |
| phase | 4 bits (low) | Session lifecycle phase (see table) |
| flags | 1 byte | Per-packet signal flags (zero during handshake) |
| payload_len | 2 bytes LE | Length of payload after phase-specific header |
### FSP Phase Table
| Phase | Type | Description |
| ----- | ---- | ----------- |
| 0x0 | Established | Post-handshake encrypted traffic or plaintext error signals |
| 0x1 | Handshake msg1 | SessionSetup (Noise XK msg1) |
| 0x2 | Handshake msg2 | SessionAck (Noise XK msg2) |
| 0x3 | Handshake msg3 | SessionMsg3 (Noise XK msg3) |
### FSP Flags (Established Phase Only)
| Bit | Name | Description |
| --- | ---- | ----------- |
| 0 | CP (coords present) | Source and destination coordinates follow the header in cleartext |
| 1 | K (key epoch) | Selects active key during rekeying |
| 2 | U (unencrypted) | Payload is plaintext (error signals) |
| 3-7 | — | Reserved (must be zero) |
Flags must be zero in handshake packets (phase 0x1, 0x2, and 0x3).
### FSP Encrypted Message (phase 0x0, U flag clear)
Post-handshake encrypted data. The 12-byte cleartext header is used as AEAD
AAD. Coordinates may appear in cleartext between the header and ciphertext
when the CP flag is set.
**Cleartext header** (12 bytes, used as AEAD AAD):
| Field | Size | Description |
| ----- | ---- | ----------- |
| common prefix | 4 bytes | ver=0, phase=0, flags, payload_len |
| counter | 8 bytes LE | Monotonic nonce, used as AEAD nonce and for replay detection |
**Optional cleartext coordinates** (when CP flag is set):
| Field | Size | Description |
| ----- | ---- | ----------- |
| src_coords_count | 2 bytes LE | Number of source coordinate entries |
| src_coords | 16 x n bytes | Source's ancestry (NodeAddr, self -> root) |
| dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
| dest_coords | 16 x m bytes | Destination's ancestry |
Transit nodes parse the CP flag and extract coordinates without decryption.
**Encrypted inner header** (6 bytes, first bytes of AEAD plaintext):
| Field | Size | Description |
| ----- | ---- | ----------- |
| timestamp | 4 bytes LE | Session-relative milliseconds (u32) |
| msg_type | 1 byte | Session-layer message type |
| inner_flags | 1 byte | Bit 0: SP (spin bit for RTT measurement) |
After the inner header, the remaining plaintext is the message-type-specific
body.
**Complete encrypted message**:
![FSP complete encrypted message](diagrams/fsp-complete-message.svg)
### FSP Session Message Types
| Type | Message | Description |
| ---- | ------- | ----------- |
| 0x10 | Data | Port-multiplexed service payload (see DataPacket below) |
| 0x11 | SenderReport | MMP sender-side metrics report |
| 0x12 | ReceiverReport | MMP receiver-side metrics report |
| 0x13 | PathMtuNotification | End-to-end path MTU echo |
| 0x14 | CoordsWarmup | Standalone coordinate cache warming |
| 0x20 | CoordsRequired | Error: transit node lacks destination coordinates |
| 0x21 | PathBroken | Error: greedy routing reached dead end |
| 0x22 | MtuExceeded | Error: forwarded packet exceeds next-hop MTU |
Message types 0x10-0x14 are carried inside the AEAD ciphertext (dispatched
by the `msg_type` field in the encrypted inner header). Types 0x20-0x22 are
plaintext error signals (U flag set, no encryption).
Session-layer SenderReport (0x11) and ReceiverReport (0x12) carry the same
metric fields as their link-layer counterparts (0x01 and 0x02), but the
body framing differs because the FSP encrypted inner header already
carries the message-type byte. The session body therefore omits the
msg_type byte and uses 2 reserved bytes (not 3) before the fields:
| Layer | Wire size | Header inside body |
| ----- | --------- | ------------------ |
| Link SenderReport (0x01) | 48 bytes | msg_type(1) + reserved(3) + fields(44) |
| Session SenderReport (0x11) | 46 bytes | reserved(2) + fields(44) |
| Link ReceiverReport (0x02) | 68 bytes | msg_type(1) + reserved(3) + fields(64) |
| Session ReceiverReport (0x12) | 66 bytes | reserved(2) + fields(64) |
Dispatch happens at the session level via the `msg_type` byte in the FSP
encrypted inner header.
### SessionSetup (phase 0x1)
Establishes a session and warms transit coordinate caches. Contains the
first message of the Noise XK handshake (ephemeral key only — the
initiator's static identity is not revealed until msg3).
SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
field in the FSP common prefix (0x1, 0x2, 0x3), not by a message type
byte. The `msg_type` field in the encrypted inner header applies only to
established-phase (0x0) messages.
Encoded with FSP prefix: ver=0, phase=0x1, flags=0, payload_len.
![SessionSetup](diagrams/session-setup.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL |
| 1 | src_coords_count | 2 bytes LE | Number of source coordinate entries |
| 3 | src_coords | 16 x n bytes | Source's ancestry (NodeAddr, self -> root) |
| ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
| ... | handshake_len | 2 bytes LE | Noise payload length |
| ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) |
### SessionAck (phase 0x2)
Second message of the Noise XK handshake. The responder sends its
ephemeral key and encrypted epoch.
Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
![SessionAck](diagrams/session-ack.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | src_coords_count | 2 bytes LE | Number of acknowledger coordinate entries |
| 3 | src_coords | 16 x n bytes | Acknowledger's ancestry (for cache warming) |
| ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries |
| ... | dest_coords | 16 x m bytes | Initiator's ancestry (for return-path cache warming) |
| ... | handshake_len | 2 bytes LE | Noise payload length |
| ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) |
### SessionMsg3 (phase 0x3)
Third and final message of the Noise XK handshake. The initiator reveals
its encrypted static identity and epoch. After msg3, both parties derive
identical symmetric session keys and the session is established.
Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
![SessionMsg3](diagrams/session-msg3.svg)
**Body** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | flags | 1 byte | Reserved |
| 1 | handshake_len | 2 bytes LE | Noise payload length |
| 3 | handshake_payload | variable | Noise XK msg3 (73 bytes — encrypted static + encrypted epoch) |
**Noise XK msg3 breakdown** (73 bytes):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
| 49 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
SessionMsg3 does not carry coordinates — both endpoints already have each
other's coordinates from SessionSetup (msg1) and SessionAck (msg2).
### Data (0x10) — DataPacket Port Multiplexing
DataPacket is the primary application data carrier. The body after the
6-byte encrypted inner header contains a 4-byte port header followed by
the service payload:
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | src_port | 2 bytes LE | Source service port |
| 2 | dst_port | 2 bytes LE | Destination service port |
| 4 | payload | variable | Service-specific payload |
The receiver dispatches by `dst_port` to the registered service handler.
**Port registry (three tiers):**
| Range | Purpose |
| ----- | ------- |
| 0255 (0x000xFF) | Reserved, protocol use |
| 2561023 (0x1000x3FF) | Reserved, FIPS standard services |
| 102465535 (0x4000xFFFF) | Application use |
**Initial assignment**: Port 256 (0x100) = IPv6 shim.
#### IPv6 Shim Payload Format (Port 256)
The IPv6 shim defines its own payload format with a leading format byte:
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | format | 1 byte | Compression format (0x00 = mesh-internal compressed) |
| 1 | fields | variable | Format-specific residual fields |
**Format 0x00 — mesh-internal compressed (default):**
Strips source and destination IPv6 addresses (32 bytes) and payload length
(2 bytes) from each packet. Carries residual fields that cannot be derived
from session context:
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | format | 1 byte | 0x00 |
| 1 | traffic_class | 1 byte | IPv6 Traffic Class (DSCP + ECN) |
| 2 | flow_label | 3 bytes | IPv6 Flow Label (20 bits, big-endian, zero-padded) |
| 5 | next_header | 1 byte | IPv6 Next Header (protocol identifier) |
| 6 | hop_limit | 1 byte | IPv6 Hop Limit |
| 7 | upper_payload | variable | Upper-layer payload (TCP, UDP, ICMPv6, etc.) |
The receiver reconstructs the full 40-byte IPv6 header from session context
(source and destination addresses derived from session npubs, version = 6,
payload length from outer packet length) plus the 6 bytes of residual fields,
then delivers the complete IPv6 packet to the TUN interface.
**Format 0x01+**: Reserved for future use (e.g., full-header gateway traffic).
**Compression savings**: 29 bytes per packet (34 bytes stripped, 7 bytes
format + residual added). Net overhead for IPv6 traffic: 77 bytes
(`FIPS_IPV6_OVERHEAD`), down from 110 bytes base DataPacket overhead.
### PathMtuNotification (0x13)
Sent by the destination to report the observed forward-path MTU.
![PathMtuNotification](diagrams/path-mtu-notification.svg)
| Offset | Field | Size | Encoding |
| ------ | ----- | ---- | -------- |
| 0 | path_mtu | 2 | u16 LE — minimum MTU observed along the forward path |
**Total body: 2 bytes** (plus FSP common prefix + encrypted header + AEAD tag).
### CoordsWarmup (0x14)
Standalone coordinate cache warming message. Sent when piggybacking coordinates
via the CP flag on a data packet would exceed the transport MTU, or as an
immediate response to CoordsRequired/PathBroken signals (rate-limited).
CoordsWarmup is an encrypted FSP message with the CP flag set and an empty
body. Transit nodes extract coordinates via the existing CP-flag parsing
path — no transit-side changes required.
**Wire format**:
```text
FSP header (12 bytes, AAD): ver=0, phase=0, flags=CP, counter, payload_len
Cleartext coords: src_coords + dst_coords (same encoding as CP flag)
AEAD ciphertext: inner_header(6) + Poly1305 tag(16) = 22 bytes
Total FSP payload: 12 + coords + 22
```
The cleartext coords section uses the same variable-length encoding as any
CP-flagged message: `src_coords_count(2) + src_coords(16×n) +
dest_coords_count(2) + dest_coords(16×m)`.
**Typical size** (depth-3 tree): 12 + (2+64+2+64) + 22 = **166 bytes** FSP
payload. With SessionDatagram + link overhead: 166 + 36 + 37 = **239 bytes**
on the wire.
### CoordsRequired (0x20)
Plaintext error signal — transit node lacks coordinates for destination.
Identified by the FSP phase field, not a separate message type byte
(same as SessionSetup/SessionAck/SessionMsg3).
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len=34.
![CoordsRequired](diagrams/coords-required.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x20 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | NodeAddr we couldn't route to |
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
**Payload size**: 34 bytes. **Total on wire**: 38 bytes (4 prefix + 34 payload).
### PathBroken (0x21)
Plaintext error signal — greedy routing reached a dead end.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len.
![PathBroken](diagrams/path-broken.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x21 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | Unreachable NodeAddr |
| 18 | reporter | 16 bytes | NodeAddr of reporting router |
| 34 | last_coords_count | 2 bytes LE | Number of stale coordinate entries |
| 36 | last_known_coords | 16 x n bytes | Stale coordinates that failed |
### MtuExceeded (0x22)
Plaintext error signal — forwarded packet exceeds the next-hop link MTU.
Sent by a transit router back to the source when a SessionDatagram cannot
be forwarded because its size exceeds the outgoing link's MTU.
Encoded with FSP prefix: ver=0, phase=0x0, U flag set, payload_len=36.
![MtuExceeded](diagrams/mtu-exceeded.svg)
**Payload** (after 4-byte FSP prefix):
| Offset | Field | Size | Description |
| ------ | ----- | ---- | ----------- |
| 0 | msg_type | 1 byte | 0x22 |
| 1 | flags | 1 byte | Reserved |
| 2 | dest_addr | 16 bytes | NodeAddr of the destination being forwarded to |
| 18 | reporter | 16 bytes | NodeAddr of the router that detected the MTU violation |
| 34 | mtu | 2 bytes LE | Bottleneck MTU at the reporting router |
**Payload size**: 36 bytes. **Total on wire**: 40 bytes (4 prefix + 36 payload).
The source uses the reported MTU to adjust its session-layer path MTU
estimate. MtuExceeded is the reactive complement to the proactive
`path_mtu` field in SessionDatagram and LookupResponse.
## Encapsulation Walkthrough
A complete picture of how application data is wrapped through each layer.
### Application Data -> Wire
Starting with an IPv6 application sending a 1024-byte TCP payload to a
destination (the original IPv6 packet at the TUN is 1064 bytes: 40-byte
header + 1024-byte payload):
```text
Layer 5: Application data
1024 bytes (TCP payload inside 1064-byte IPv6 packet)
Layer 4: IPv6 shim compression (port 256)
Strip IPv6 addresses (32) + payload length (2), keep residual fields
format (1) + residual (6) + upper payload (1024) = 1031 bytes
Layer 3: Session encryption (FSP)
FSP header (12) + AEAD(inner_hdr (6) + port_hdr (4) + shim (1031)) + tag (16)
= 12 + 1041 + 16 = 1069 bytes
Layer 2: SessionDatagram envelope (FMP routing)
msg_type (1) + ttl (1) + path_mtu (2) + src_addr (16) + dest_addr (16) + payload (1069)
= 1105 bytes
Layer 1: Link encryption (FMP per-hop)
outer header (16) + encrypted(inner_hdr (5) + datagram (1105)) + AEAD tag (16)
= 1142 bytes
Layer 0: Transport
UDP datagram containing 1142 bytes
```
Total overhead for IPv6 traffic: 1142 1064 = 78 bytes per packet. The
difference from the `FIPS_IPV6_OVERHEAD` constant (77 bytes) is the 1-byte
FMP `msg_type` counted in the link inner header rather than the
SessionDatagram body.
### Overhead Budget
| Layer | Overhead | Component |
| ----- | -------- | --------- |
| Link encryption | 37 bytes | 16 outer header (AAD) + 5 inner header (timestamp + msg_type) + 16 AEAD tag |
| SessionDatagram body | 35 bytes | 1 ttl + 2 path_mtu + 16 src + 16 dest (msg_type counted in inner header) |
| FSP header | 12 bytes | 4 prefix + 8 counter |
| FSP inner header | 6 bytes | 4 timestamp + 1 msg_type + 1 inner_flags (inside AEAD) |
| Session AEAD tag | 16 bytes | Poly1305 tag on session-encrypted payload |
| **Protocol envelope** | **106 bytes** | `FIPS_OVERHEAD` constant |
| Port header | 4 bytes | src_port + dst_port (DataPacket only) |
| **DataPacket total** | **110 bytes** | Base overhead for any port-multiplexed service |
| IPv6 compression | 33 bytes | 40-byte IPv6 header → 7-byte format + residual |
| **IPv6 data path total** | **77 bytes** | `FIPS_IPV6_OVERHEAD` constant |
### At Each Transit Node
```text
1. Receive UDP datagram
2. Parse common prefix -> version, phase, flags, payload_len
3. Phase 0x0 -> established frame
4. Look up (transport_id, receiver_idx) -> session
5. Check replay window (counter)
6. Decrypt with link keys (16-byte header as AAD) -> plaintext
7. Strip inner header -> timestamp, msg_type
8. msg_type 0x00 -> SessionDatagram
9. Read dest_addr -> routing decision
10. Decrement ttl, min path_mtu
11. Re-encrypt with next-hop link keys
12. Send via next-hop transport
```
Transit nodes see the SessionDatagram envelope (src_addr, dest_addr,
ttl, path_mtu) but cannot read the session-layer payload (encrypted with
endpoint session keys).
## Size Summary
### FMP Handshake Messages (Noise IK)
| Message | Raw Noise | Wire Frame |
| ------- | --------- | ---------- |
| IK msg1 (ephemeral + encrypted static + encrypted epoch) | 106 bytes | 114 bytes |
| IK msg2 (ephemeral + encrypted epoch) | 57 bytes | 69 bytes |
### FSP Handshake Messages (Noise XK)
| Message | Raw Noise | Notes |
| ------- | --------- | ----- |
| XK msg1 (ephemeral only) | 33 bytes | Carried in SessionSetup |
| XK msg2 (ephemeral + encrypted epoch) | 57 bytes | Carried in SessionAck |
| XK msg3 (encrypted static + encrypted epoch) | 73 bytes | Carried in SessionMsg3 |
### Link-Layer Messages (inside encrypted frame)
| Message | Size | Notes |
| ------- | ---- | ----- |
| TreeAnnounce | 100 + 32n bytes | n = depth + 1 |
| FilterAnnounce | 1,035 bytes | v1 (1KB filter) |
| LookupRequest | 46 + 16n bytes | n = origin depth + 1 |
| LookupResponse | 93 + 16n bytes | n = target depth + 1 |
| SessionDatagram | 36 + payload bytes | Fixed 36-byte header |
| Disconnect | 2 bytes | |
### Session-Layer Messages (inside SessionDatagram)
| Message | Typical Size | Notes |
| ------- | ------------ | ----- |
| SessionSetup | ~170 bytes | Depth-dependent (XK msg1 = 33 bytes) |
| SessionAck | ~190 bytes | Depth-dependent, carries both endpoints' coords (XK msg2 = 57 bytes) |
| SessionMsg3 | ~80 bytes | Fixed (XK msg3 = 73 bytes, no coords) |
| Data (minimal) | 12 + 6 + 4 + payload + 16 bytes | Steady state (port header included) |
| Data (with coords) | 12 + ~130 + 6 + 4 + payload + 16 bytes | Warmup/recovery (port header included) |
| SenderReport | 12 + 6 + 46 + 16 bytes | MMP metrics |
| ReceiverReport | 12 + 6 + 66 + 16 bytes | MMP metrics |
| PathMtuNotification | 12 + 6 + 2 + 16 bytes | MTU signal |
| CoordsWarmup | 12 + coords + 6 + 16 bytes | Standalone warmup (empty body) |
| CoordsRequired | 38 bytes | Fixed (prefix + msg_type + body) |
| PathBroken | 35 + 16n bytes | Includes stale coords |
| MtuExceeded | 40 bytes | Fixed (prefix + msg_type + body) |
### Complete Packet Sizes (link + session)
| Scenario | Wire Size | Notes |
| -------- | --------- | ----- |
| Encrypted frame minimum | 37 bytes | Empty body |
| SessionDatagram + Data (minimal) | 37 + 35 + 12 + 6 + 4 + payload + 16 | 110 + payload (any service) |
| SessionDatagram + IPv6 Data (minimal) | 110 + 7 + upper_payload 34 | 77 + IPv6 payload (compressed) |
| SessionDatagram + Data (with coords) | 110 + coords + payload | Coords vary with tree depth |
| SessionDatagram + SessionSetup | ~275 bytes | Depth-3, both dirs |
| SessionDatagram + CoordsRequired | 37 + 36 + 38 = 111 bytes | Including link overhead |
## References
- [../design/fips-mesh-layer.md](../design/fips-mesh-layer.md) — FMP behavioral specification
- [../design/fips-session-layer.md](../design/fips-session-layer.md) — FSP behavioral specification
- [../design/fips-transport-layer.md](../design/fips-transport-layer.md) — Transport framing
- [../design/fips-mesh-operation.md](../design/fips-mesh-operation.md) — How messages work together
- [../design/fips-ipv6-adapter.md](../design/fips-ipv6-adapter.md) — MTU enforcement
- [../design/fips-bloom-filters.md](../design/fips-bloom-filters.md) — FilterAnnounce parameters and FPR analysis
- [../design/fips-mtu.md](../design/fips-mtu.md) — How `path_mtu` and MtuExceeded fit together