Merge branch 'master' into next

Forward-merge of the docs-overhaul squash (5abf9a9) and top-level
README rewrite (18019bb). Conflict resolution:

- README.md: master's rewritten feature lists adopted, with the encryption
  bullets reflecting next's two-layer Noise XX (replacing the IK/XK pair
  master describes for v0.3.0).
- 6 design/reference markdown files (fips-bloom-filters.md, fips-mesh-layer.md,
  fips-mesh-operation.md, fips-session-layer.md, fips-transport-layer.md,
  reference/wire-formats.md): master's reorg taken, next's protocol details
  preserved (XX handshake naming, bloom v2 RLE/delta wire format,
  v2 LookupRequest sizing).
- fips-intro.md modify/delete: accepted master's split into
  fips-architecture.md / fips-concepts.md / fips-prior-work.md, then
  re-applied next's IK/XK -> XX transition and spin-bit removal across
  the relevant split files. Same pass swept docs/reference/security.md,
  docs/design/fips-mmp.md, docs/design/fips-security.md,
  docs/design/fips-nostr-discovery.md,
  docs/design/port-advertisement-and-nat-traversal.md,
  docs/how-to/enable-nostr-discovery.md, and the affected tutorials so
  no IK/XK or spin-bit prose remains in current-state docs.
- Diagram path conflicts: noise-ik-msg{1,2}.svg removed (IK is gone);
  noise-xx-msg{1,2,3}.svg moved from docs/design/diagrams/ to
  docs/reference/diagrams/ to match master's diagram reorg. The
  wire-formats.md image references resolve correctly to the new path.

Local verification: cargo build --release, cargo test (1265 passed,
4 ignored), cargo clippy -D warnings, cargo fmt --check all green.
This commit is contained in:
Johnathan Corgan
2026-05-08 13:45:04 +00:00
92 changed files with 11631 additions and 3608 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,52 @@
<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">FilterAnnounce (0x20) &#8212; 19-byte header + RLE payload</text>
<!-- Row 0 (0-1): msg_type + flags -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;1</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
<rect x="185" y="36" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="380" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
<text x="380" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte &#8212; bit 0: delta (XOR diff)</text>
<!-- Row 1 (2-9): sequence -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">2&#8211;9</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 &#8212; per-peer monotonic counter</text>
<!-- Row 2 (10-17): base_seq -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">10&#8211;17</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">base_seq</text>
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE &#8212; reference sequence for delta (0 if full)</text>
<!-- Row 3 (18): size_class -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">18</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">size_class</text>
<text x="315" y="232" fill="#8af8f8" text-anchor="middle" font-size="10">1 byte &#8212; filter size = 512 &#171; size_class bytes</text>
<!-- Row 4 (19-): compressed_payload -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">19&#8211;</text>
<rect x="55" y="250" width="520" height="40" fill="#1f1f3a" stroke="#4a9090" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
<text x="315" y="274" fill="#8ad8d8" text-anchor="middle" font-size="11">compressed_payload (RLE: [count:2 LE][word:8 LE] per run)</text>
<!-- Explanation -->
<rect x="55" y="304" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="324" fill="#888" text-anchor="middle" font-size="10">delta: XOR diff of current vs last-sent filter &#8212; full: raw filter words</text>
<!-- Total -->
<text x="310" y="362" fill="#777" font-size="10" text-anchor="middle">19-byte header + variable compressed payload</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 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,75 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 620" 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>
</defs>
<!-- Background -->
<rect width="620" height="620" 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 XX)</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="600" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
<line x1="520" y1="62" x2="520" y2="600" 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="145" y="128" width="330" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x11|flags=0|len] | sender_idx | noise_msg1</text>
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 &#8212; 41 bytes &#8212; pattern: &#8594; e</text>
<!-- Step 2: Responder processes msg1 -->
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1 (ephemeral only)</text>
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
<!-- Arrow 2: msg2 (responder -> initiator) -->
<line x1="520" y1="244" x2="100" y2="244" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="252" width="390" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
<text x="310" y="268" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x12|flags=0|len] | sender_idx | receiver_idx | noise_msg2 + negotiation</text>
<text x="310" y="288" fill="#666" text-anchor="middle" font-size="9">phase 0x2 &#8212; 144 bytes &#8212; pattern: &#8592; e, ee, s, es</text>
<text x="600" y="300" fill="#d8a0f8" font-size="9" text-anchor="end">responder identity revealed</text>
<!-- Step 3: Initiator processes msg2, learns responder identity -->
<text x="20" y="322" fill="#8af8c8" font-size="10">validates msg2</text>
<text x="20" y="336" fill="#8af8c8" font-size="10">learns responder identity</text>
<text x="20" y="350" fill="#8af8c8" font-size="10">checks epoch (restart detection)</text>
<!-- Arrow 3: msg3 (initiator -> responder) -->
<line x1="100" y1="368" x2="520" y2="368" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="376" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
<text x="310" y="392" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x13|flags=0|len] | sender_idx | receiver_idx | noise_msg3 + neg</text>
<text x="310" y="412" fill="#666" text-anchor="middle" font-size="9">phase 0x3 &#8212; 111 bytes &#8212; pattern: &#8594; s, se</text>
<text x="20" y="424" fill="#d8a0f8" font-size="9">initiator identity revealed</text>
<!-- Step 4: Responder processes msg3, learns initiator identity -->
<text x="600" y="440" fill="#8af8c8" font-size="10" text-anchor="end">validates msg3</text>
<text x="600" y="454" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator identity</text>
<text x="600" y="468" fill="#8af8c8" font-size="10" text-anchor="end">checks epoch, derives session keys</text>
<!-- Handshake complete separator -->
<line x1="30" y1="488" x2="230" y2="488" stroke="#d9904a" stroke-width="1.5"/>
<text x="310" y="492" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
<line x1="390" y1="488" x2="590" y2="488" stroke="#d9904a" stroke-width="1.5"/>
<!-- Arrow 4: first encrypted frame -->
<text x="20" y="516" fill="#777" font-size="10">first encrypted frame:</text>
<line x1="100" y1="530" x2="520" y2="530" stroke="#d9b04a" stroke-width="1.5" marker-end="url(#arrowR)"/>
<rect x="115" y="538" width="390" height="24" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1" rx="3"/>
<text x="310" y="554" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
<text x="310" y="574" fill="#666" text-anchor="middle" font-size="9">phase 0x0 &#8212; established frame</text>
<!-- Legend -->
<text x="310" y="600" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch + negotiation exchanged in msg2/msg3.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.1 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

+43
View File
@@ -0,0 +1,43 @@
<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">Noise XX Message 1 &#8212; phase 0x1 (41 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</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">4&#8211;7</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: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;40</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Total -->
<text x="310" y="210" fill="#777" font-size="10" text-anchor="middle">total: 41 bytes &#183; payload_len = 37 &#183; pattern: &#8594; e</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+82
View File
@@ -0,0 +1,82 @@
<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">Noise XX Message 2 &#8212; phase 0x2 (118&#8211;144 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</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">4&#8211;7</text>
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- receiver_idx (bytes 8-11) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end"/>
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise XX msg2 bracket -->
<text x="10" y="290" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 290)">Noise XX msg2 (106 bytes base)</text>
<line x1="18" y1="140" x2="18" y2="400" stroke="#555" stroke-width="1"/>
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
<line x1="18" y1="400" x2="23" y2="400" stroke="#555" stroke-width="1"/>
<!-- Row 2: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12&#8211;44</text>
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
<!-- Row 3: encrypted_static (bytes 45-93, 49 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">45&#8211;93</text>
<rect x="55" y="192" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<text x="315" y="234" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 4: encrypted_epoch (bytes 94-117, 24 bytes) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">94&#8211;117</text>
<rect x="55" y="244" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="286" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Row 5: negotiation payload (variable, optional) -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">118+</text>
<rect x="55" y="296" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
<text x="315" y="338" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
<!-- Row 6: AEAD explanation -->
<rect x="55" y="362" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="386" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() &#8212; extends Noise hash chain</text>
<!-- Total -->
<text x="310" y="425" fill="#777" font-size="10" text-anchor="middle">base: 118 bytes &#183; with FMP negotiation: 144 bytes</text>
<text x="310" y="442" fill="#777" font-size="10" text-anchor="middle">pattern: &#8592; e, ee, s, es</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

+72
View File
@@ -0,0 +1,72 @@
<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 XX Message 3 &#8212; phase 0x3 (85&#8211;111 bytes)</text>
<!-- Row 0: Common prefix (bytes 0-3) -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</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 + receiver_idx (bytes 4-11) -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;11</text>
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Noise XX msg3 bracket -->
<text x="10" y="240" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 240)">Noise XX msg3 (73 bytes base)</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"/>
<!-- Row 2: encrypted_static (bytes 12-60, 49 bytes) -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12&#8211;60</text>
<rect x="55" y="140" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
<text x="315" y="182" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
<!-- Row 3: encrypted_epoch (bytes 61-84, 24 bytes) -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">61&#8211;84</text>
<rect x="55" y="192" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
<text x="315" y="234" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
<!-- Row 4: negotiation payload (variable, optional) -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">85+</text>
<rect x="55" y="244" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
<text x="315" y="286" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
<!-- Row 5: explanation -->
<rect x="55" y="310" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="334" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() &#8212; extends Noise hash chain</text>
<!-- Total -->
<text x="310" y="378" fill="#777" font-size="10" text-anchor="middle">base: 85 bytes &#183; with FMP negotiation: 111 bytes</text>
<text x="310" y="395" fill="#777" font-size="10" text-anchor="middle">pattern: &#8594; s, se</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 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

@@ -0,0 +1,97 @@
<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) &#8212; 54 bytes</text>
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 50)</text>
<!-- Row 1 (4-7): timestamp_echo -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</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">timestamp_echo</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE (echoed sender timestamp for RTT)</text>
<!-- Row 2 (8-9): dwell_time -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;9</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">dwell_time</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE (ms between receive and echo)</text>
<!-- Row 3 (10-17): highest_counter -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">10&#8211;17</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">highest_counter</text>
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 4 (18-25): cumulative_packets_recv -->
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">18&#8211;25</text>
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
<text x="315" y="284" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 5 (26-33): cumulative_bytes_recv -->
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">26&#8211;33</text>
<rect x="55" y="296" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
<text x="315" y="336" fill="#8af8f8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Row 6 (34-41): jitter + ecn_ce_count -->
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">34&#8211;41</text>
<rect x="55" y="348" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" 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="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE (&#181;s)</text>
<rect x="315" y="348" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" 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="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 7 (42-49): owd_trend + burst_loss_count -->
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">42&#8211;49</text>
<rect x="55" y="400" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" 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="#f8c88a" text-anchor="middle" font-size="10">i32 LE (&#181;s/s)</text>
<rect x="315" y="400" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" 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="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 8 (50-53): cumulative_reorder_count -->
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">50&#8211;53</text>
<rect x="55" y="452" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="315" y="474" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_reorder_count</text>
<text x="315" y="492" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Forward compat note -->
<rect x="55" y="518" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="538" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
<!-- Removed fields note -->
<rect x="55" y="558" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="578" fill="#666" text-anchor="middle" font-size="10">removed from v0.2: max_burst_loss, mean_burst_loss, interval_packets/bytes_recv</text>
<!-- Total -->
<text x="310" y="618" fill="#777" font-size="10" text-anchor="middle">total: 54 bytes &#183; format_version = 0 &#183; total_length = 50</text>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

+50
View File
@@ -0,0 +1,50 @@
<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">SenderReport (0x01) &#8212; 20 bytes</text>
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0&#8211;3</text>
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 16)</text>
<!-- Row 1 (4-7): interval_packets_sent -->
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4&#8211;7</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_packets_sent</text>
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 2 (8-11): interval_bytes_sent -->
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8&#8211;11</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">interval_bytes_sent</text>
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
<!-- Row 3 (12-19): cumulative_packets_sent -->
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">12&#8211;19</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_packets_sent</text>
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
<!-- Forward compat note -->
<rect x="55" y="258" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
<text x="315" y="278" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
<!-- Total -->
<text x="310" y="314" fill="#777" font-size="10" text-anchor="middle">total: 20 bytes &#183; format_version = 0 &#183; total_length = 16</text>
</svg>

After

Width:  |  Height:  |  Size: 3.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">SessionAck (phase 0x2) &#8212; Noise XX 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 XX msg2 (106+ bytes &#8212; ephemeral + static + epoch)</text>
<!-- Total -->
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~240 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) &#8212; Noise XX 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 XX msg3 (73+ bytes &#8212; encrypted static + epoch + optional negotiation)</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) &#8212; Noise XX 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 XX msg1 (33 bytes &#8212; 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 XX, FSP XX, Schnorr signatures |
| Diffie-Hellman | ECDH on secp256k1 (x-only normalized) | Noise XX (both layers) |
| 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_XX_secp256k1_ChaChaPoly_SHA256` | FMP link layer (XX with negotiation payload) |
| Noise pattern (session) | `Noise_XX_secp256k1_ChaChaPoly_SHA256` | FSP session layer (XX with negotiation 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 XX 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 XX, 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
File diff suppressed because it is too large Load Diff