Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ docs/
├── lookup/ — lookup command documentation
├── announce/ — announce command documentation (echo protocol defined here)
├── ping/ — ping command documentation (cross-refs echo protocol)
├── node/ — node command documentation
├── cp/ — cp command documentation
├── dd/ — dd (Dead Drop) command documentation (v1 + v2 protocols)
├── chat/ — chat subsystem (user guide, TUI, wire format, protocol, reference)
Expand All @@ -39,6 +40,7 @@ Output goes to `docs/book/` (gitignored).
## Conventions

- All Mermaid diagrams use ` ```mermaid ``` ` fences — rendered by `mdbook-mermaid`.
- Command chapters live under `src/<command>/` with an `overview.md` entry in `SUMMARY.md`; follow the same pattern for new commands such as `relay/`.
- Cross-references use relative `[text](../path/to/file.md)` links (mdBook requirement).
- Human output examples go on **stderr**; structured JSON output goes on **stdout**.
- The Echo Protocol is defined exactly once in `src/announce/echo-protocol.md`. All other chapters that reference it must link there rather than re-documenting it.
Expand Down
3 changes: 3 additions & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@

# Commands

- [node](./node/overview.md)
- [relay](./relay/overview.md)
- [Architecture](./relay/architecture.md)
- [lookup](./lookup/overview.md)
- [Output Formats](./lookup/output-formats.md)
- [announce](./announce/overview.md)
Expand Down
11 changes: 10 additions & 1 deletion docs/src/appendices/tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,21 @@ The 8 natural subsystems and the targets that feed them:
| holepunch | `peeroxide_dht::holepuncher` | `peeroxide::_events::holepunch::*` |
| nat | `peeroxide_dht::nat` | _(none currently)_ |
| socket_pool | `peeroxide_dht::socket_pool` | _(none currently)_ |
| relay | `peeroxide_dht::blind_relay` | _(none currently)_ |
| relay | `peeroxide::cmd::relay`, `peeroxide::cmd::node`, `peeroxide_dht::relay_service`, `peeroxide_dht::blind_relay` | _(none currently; module-path logs only)_ |
| discovery | `peeroxide_dht::query`, `peeroxide::peer_discovery` | _(none currently)_ |
| swarm | `peeroxide::swarm` | `peeroxide::_events::swarm::*`, `peeroxide::_events::peer::*` |
| dht_rpc | `peeroxide_dht::rpc`, `peeroxide_dht::io` | `peeroxide::_events::dht::*` |
| udx | `libudx::native::*` | _(none currently)_ |

Relay is intentionally module-path only today. `peeroxide::cmd::relay`
and `peeroxide::cmd::node` emit the operator-facing startup / shutdown /
periodic relay stats at `info`; `peeroxide_dht::relay_service` emits
self-announce success/failure, handshake/finalization, capacity
rejections, and bridge failures at `debug`/`warn`; and
`peeroxide_dht::blind_relay` emits pair request/response activity,
pairing matches, idle-session / expired-pairing sweeps, and malformed
frame drops at `debug`/`trace`.

New `_events::*` labels should be added sparingly, only when an event
represents an operator-visible lifecycle transition (something a
production operator would want in a clean default-level log).
Expand Down
5 changes: 3 additions & 2 deletions docs/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@ The binary is named `peeroxide`.

## Core Tools

The toolkit consists of eight primary commands:
The toolkit consists of nine primary commands:

- **[init](./init/overview.md)**: Generate configuration files and install man pages.
- **[node](./node/overview.md)**: Run a long-running DHT bootstrap / coordination node, optionally with a courtesy blind-relay.
- **[relay](./relay/overview.md)**: Run a standalone blind-relay server that bridges two NATed/firewalled peers who can't connect directly.
- **[lookup](./lookup/overview.md)**: Query the DHT to find peers announcing a specific topic.
- **[announce](./announce/overview.md)**: Announce your presence on a topic so others can discover you.
- **[ping](./ping/overview.md)**: Diagnose reachability through bootstrap checks, NAT classification, or targeted peer pings.
- **[cp](./cp/overview.md)**: Transfer files directly between peers over an encrypted swarm connection.
- **[dd (Dead Drop)](./dd/overview.md)**: Perform anonymous store-and-forward messaging via the DHT. The `dd` command supports both v1 and v2 protocols, with v2 auto-selected for new put operations.
- **[chat](./chat/overview.md)**: Join topic-based interactive chat rooms.
- **node**: Run a long-running DHT bootstrap / coordination node.

## Quick Start

Expand Down
128 changes: 128 additions & 0 deletions docs/src/node/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Node Overview

The `node` command runs a long-lived DHT routing node. It joins the HyperDHT-compatible network, maintains a routing table, answers DHT queries, and stores the short-lived records that power `lookup`, `announce`, and higher-level commands built on top of them.

For background on how routing works, see [DHT and Routing](../concepts/dht-and-routing.md) and [DHT Primitives](../concepts/dht-primitives.md).

## Why run a node?

Running `peeroxide node` is useful when you want to:

- contribute stable routing capacity to the network,
- self-host one or more bootstrap addresses for your own deployment,
- build a private or test mesh with explicitly chosen bootstrap peers,
- keep DHT infrastructure running alongside your own `announce`, `cp`, `dd`, or `chat` usage.

A bootstrap node is not a separate protocol role. It is just a node with a stable address that other peers use as an entry point.

## Usage

```bash
peeroxide node [FLAGS]
```

## Common examples

Run a node with the default network settings:

```bash
peeroxide node
```

Run a public-facing node on the standard HyperDHT port:

```bash
peeroxide node --public --host 0.0.0.0 --port 49737
```

Run a node in a private mesh using only explicit bootstrap peers:

```bash
peeroxide node \
--no-public \
--bootstrap 10.0.0.11:49737 \
--bootstrap 10.0.0.12:49737 \
--host 0.0.0.0 \
--port 49737
```

Run a routing node that also offers courtesy blind-relay service:

```bash
peeroxide node --public --relay -v
```

## Bootstrap and network selection

`node` uses the same bootstrap-resolution rules as the other DHT commands. The inherited global flags are documented in [init](../init/overview.md#global-cli-flags).

In practice:

- `--bootstrap <ADDR>` supplies explicit bootstrap addresses. Repeat it for multiple peers.
- `--public` adds the default public HyperDHT bootstrap nodes.
- `--no-public` removes the default public bootstrap nodes.
- If you provide neither custom bootstraps nor `--no-public`, peeroxide auto-fills the public bootstrap set.

That means `peeroxide node --no-public` with no `--bootstrap` starts in isolated mode: it listens for inbound peers, but it has no built-in way to discover the wider network.

## Flags

### Node-specific flags

| Flag | Description |
|---|---|
| `--port <PORT>` | UDP bind port. Default: `49737`. |
| `--host <HOST>` | UDP bind address. Default: `0.0.0.0`. |
| `--stats-interval <SECONDS>` | Interval for periodic stats logging. Default: `60`. Must be greater than `0`. |
| `--max-records <N>` | Maximum number of announcement records the node stores. |
| `--max-lru-size <N>` | Maximum number of mutable/immutable cache entries. |
| `--max-per-key <N>` | Maximum peer announcements stored for a single topic. |
| `--max-record-age <SECONDS>` | TTL for announcement records. |
| `--max-lru-age <SECONDS>` | TTL for mutable/immutable cache entries. |

These storage and TTL knobs affect how much DHT state your node retains while serving the network.

### Relay-related flags

| Flag | Description |
|---|---|
| `--relay` | Also run a courtesy blind-relay server inside this node process. |
| `--relay-key-seed <64-hex>` | Deterministic 32-byte seed for the relay identity keypair. Without it, a fresh relay keypair is generated on each run. |
| `--relay-max-sessions <N>` | Maximum concurrently accepted relay sessions. Default: `10000`. |
| `--relay-max-pairings-per-session <N>` | Maximum pending + active pairings per relay session. Default: `256`. |
| `--relay-pairing-timeout <SECONDS>` | Drop an unmatched relay pairing after this many seconds. Default: `300`. |
| `--relay-idle-session-timeout <SECONDS>` | Close a relay session with no pair/unpair activity for this many seconds. Default: `600`. |

### Inherited global flags

`node` also accepts the shared top-level flags:

- `--config <FILE>`
- `--no-default-config`
- `--public`
- `--no-public`
- `--bootstrap <ADDR>`
- `-v` / `-vv`

## Relay mode

When you add `--relay`, the process still behaves as a normal DHT node. It simply adds a blind-relay service alongside its routing duties.

This is useful when you want one always-on process to do both jobs: participate in routing and offer a courtesy relay endpoint for peers that cannot connect directly. For a dedicated relay-only process, see the [relay command](../relay/overview.md).

If relay mode is enabled, startup prints the relay public key to stdout before the bound `host:port` line.

## Operational notes

- **Long-running process**: `node` is meant to stay up. It exits cleanly on Ctrl-C or SIGTERM.
- **Output streams**: the bound address is printed to stdout. Tracing and operational logs go to stderr.
- **Verbosity**: `-v` enables info-level lifecycle logs such as bootstrap completion and relay counters. `-vv` additionally enables periodic routing-table and cache statistics.
- **Private meshes**: for test or lab networks, combine `--no-public` with explicit `--bootstrap` peers. The repo's Docker NAT test uses a multi-node private mesh rather than a single bootstrap address so routing information converges faster.
- **Multiple nodes**: if you are building your own bootstrap set, run more than one stable node when possible. A small cluster is more robust than a single entry point.

## See Also

- [DHT and Routing](../concepts/dht-and-routing.md)
- [DHT Primitives](../concepts/dht-primitives.md)
- [Ping Overview](../ping/overview.md)
- [Announce Overview](../announce/overview.md)
145 changes: 145 additions & 0 deletions docs/src/relay/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Relay Architecture

This chapter describes how peeroxide's blind-relay implementation works internally.

At a high level, the relay has two separate jobs:

1. accept an authenticated control connection from each peer,
2. once both sides present the same relay token, create two raw UDX streams and bridge them.

The relay never terminates the application protocol spoken by the two real peers.

## Control plane vs. data plane

The control plane is a normal HyperDHT connection to the relay node. Each side completes the relay's own Noise/SecretStream session, then opens a Protomux channel named `"blind-relay"`.

The data plane is different. After the relay matches a token, it creates two UDX streams and wires them together with `UdxStream::relay_to` in both directions. Those forwarded packets belong to the real peer-to-peer session, not to the relay's control channel.

```mermaid
sequenceDiagram
participant A as Peer A
participant R as Relay
participant B as Peer B

A->>R: HyperDHT + Noise control connection
B->>R: HyperDHT + Noise control connection
A->>R: Protomux "blind-relay" pair(token, stream_id)
B->>R: Protomux "blind-relay" pair(token, stream_id)
R-->>A: pair matched, relay stream id
R-->>B: pair matched, relay stream id
R->>R: create two UDX streams
R->>R: relay_to in both directions
A->>R: encrypted UDX packets
R->>B: forwarded encrypted packets
```

## The pairing model

The protocol engine lives in `peeroxide-dht/src/blind_relay.rs`.

- `BlindRelayServer` owns the shared pairing tables and limits for the whole relay instance.
- `BlindRelaySession` represents one accepted control connection.
- `BlindRelayClient` is the client-side helper used by a peer connecting through a relay.

Each `pair` message carries:

- a 32-byte token,
- an `is_initiator` bit,
- the sender's local UDX stream id.

The relay keeps up to two pending links per token: one initiator slot and one responder slot. When both are present, the token is considered matched and the relay emits a `MatchedPairing` record to the transport layer.

That transport layer lives in `peeroxide-dht/src/relay_service.rs`, which creates the actual streams, bridges them, and notifies both sessions of the relay-assigned stream ids to use.

`unpair` removes a pending token or tears down an already-active bridged pairing.

## Why the relay is “blind”

The important boundary is this:

- the relay's **control connections** are encrypted so each side can authenticate to the relay and exchange pair/unpair messages safely,
- the **relayed application payload** is already end-to-end encrypted between the two real peers.

So the relay is not providing payload confidentiality. It is forwarding ciphertext that was produced for the real peer-to-peer session.

In peeroxide's transport wiring, that forwarding happens at the UDX packet level through `relay_to`. The relay never unwraps the application's SecretStream frames and never learns the plaintext.

## Server-side swarm behavior

On the client side, blind relay is integrated into `peeroxide::Swarm`.

When a swarm is configured with `SwarmConfig::relay_through`, the server side changes its handshake reply:

- it mints a fresh relay token,
- it includes `relay_through` metadata naming the relay public key,
- it optionally includes `relay_addresses` when `SwarmConfig::relay_address` is known,
- it skips the normal holepunch path for that connection.

Later, once the server receives the inbound handshake, it calls `create_server_relay_connection` to:

1. connect to the relay over HyperDHT,
2. open a `BlindRelayClient` on the control channel,
3. pair with the token,
4. open the relayed UDX data stream,
5. wrap that data stream with the already-negotiated peer-to-peer SecretStream session.

That last step is why the relay does not become the endpoint of the application encryption.

## Session limits and timeouts

Peeroxide adds configurable operational limits around the Node-compatible relay protocol:

| Setting | Default | Effect |
|---|---:|---|
| `max_sessions` | `10000` | Maximum concurrent accepted relay sessions. |
| `max_pairings_per_session` | `256` | Caps how many pending + active tokens one session can hold. |
| `pairing_timeout` | `300s` | Drops a token if the matching side never arrives. |
| `idle_session_timeout` | `600s` | Closes a session with no pair/unpair activity. |

These live in `BlindRelayServerConfig`.

Node's reference `blind-relay` package does **not** have equivalents for those limits or timeouts. Peeroxide adds them as hardening measures with generous defaults so a default deployment still behaves much like the unthrottled reference implementation.

## Teardown behavior

Once a pairing is active, peeroxide tracks the bridged stream pair by token.

Two teardown paths matter:

1. **Unpair on an active token**: the relay stops forwarding and drops the bridged streams.
2. **Session close**: if either control session closes, every active pairing owned by that session is torn down.

This teardown behavior has direct precedent in Node's reference implementation: Node's `blind-relay` destroys active streams in its `_onunpair` and `_onclose` paths.

The idle-session sweep is different. That is a peeroxide-only addition with no Node precedent.

## Maintenance loops

`relay_service::run_relay_server` runs two background maintenance loops in addition to handshake acceptance:

- a periodic self-announce refresh loop for the relay's own identity,
- a periodic sweep loop that expires stale pending pairings and closes idle sessions.

The sweep loop is what enforces `pairing_timeout` and `idle_session_timeout` in practice.

## Self-announcement and discovery

One subtle but important implementation detail is that the relay must do more than call `register_server` locally.

`register_server` only teaches the local DHT router to handle inbound `PEER_HANDSHAKE` requests for `hash(public_key)` if they somehow arrive. It does **not** tell the rest of the network that this relay exists.

Peeroxide therefore self-announces the relay's own identity target, `hash(public_key)`, at startup and on a roughly 10-minute refresh loop. This mirrors Node's `Server.listen()` behavior.

That matters for interoperability. A Node.js `hyperdht` client connecting by relay public key does discovery through a lookup-style `findPeer` query for an announced record on `hash(public_key)`. Without the self-announce, the relay could be perfectly healthy locally but still undiscoverable remotely, producing `PEER_NOT_FOUND`.

## Why `relay_service` is separate from `Swarm`

A relay is not a topic participant. It does not join discovery topics, manage peer sets, or maintain swarm retry state.

For that reason, peeroxide keeps the relay transport wiring in `peeroxide-dht::relay_service` as a small accept-and-bridge service built directly on HyperDHT and UDX, while `peeroxide::Swarm` consumes the relay from the client side.

## See Also

- [Relay Overview](overview.md)
- [DHT and Routing](../concepts/dht-and-routing.md)
- [Security Model](../appendices/security-model.md)
Loading
Loading