docs: comprehensive documentation — design, architecture, admin, user guide
4 files, 2,511 lines covering the entire WarzonePhone project: DESIGN.md (591 lines): system overview, codec system (9 variants), FEC (RaptorQ), transport (QUIC/quinn), security (Ed25519/X25519/ ChaCha20/HKDF/BIP39/TOFU), federation (global rooms), jitter buffer. Mermaid diagrams for audio pipelines and crate dependencies. ARCHITECTURE.md (874 lines): 15 mermaid diagrams — system overview, encode/decode pipelines, relay SFU, federation topology/protocol, signal handshake, client architectures (desktop/android/CLI), wire format tables (MediaHeader/MiniHeader/QualityReport), project tree. ADMINISTRATION.md (587 lines): relay deployment (binary/Docker/systemd), complete TOML config reference, CLI flags table, federation setup (peers/trusted/global_rooms), 3 example configs, Prometheus metrics, auth, identity persistence, 12-item troubleshooting guide. USER_GUIDE.md (459 lines): all clients — desktop (settings, quality slider, key warning, shortcuts), Android (8-level quality slider, server management, identity backup), CLI (flags table, 8 usage patterns). Identity system, quality profiles when-to-use guide. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
587
docs/ADMINISTRATION.md
Normal file
587
docs/ADMINISTRATION.md
Normal file
@@ -0,0 +1,587 @@
|
||||
# WarzonePhone Relay Administration Guide
|
||||
|
||||
This document covers deploying, configuring, and operating wzp-relay instances, including federation setup, monitoring, and troubleshooting.
|
||||
|
||||
## Relay Deployment
|
||||
|
||||
### Binary
|
||||
|
||||
Build and run the relay directly:
|
||||
|
||||
```bash
|
||||
# Build release binary
|
||||
cargo build --release --bin wzp-relay
|
||||
|
||||
# Run with defaults (listen on 0.0.0.0:4433, room mode, no auth)
|
||||
./target/release/wzp-relay
|
||||
|
||||
# Run with config file
|
||||
./target/release/wzp-relay --config /etc/wzp/relay.toml
|
||||
```
|
||||
|
||||
### Remote Build (Linux)
|
||||
|
||||
The included build script provisions a temporary Hetzner Cloud VPS, builds all binaries, and downloads them:
|
||||
|
||||
```bash
|
||||
# Requires: hcloud CLI authenticated, SSH key "wz" registered
|
||||
./scripts/build-linux.sh
|
||||
# Outputs to: target/linux-x86_64/
|
||||
```
|
||||
|
||||
Produces: `wzp-relay`, `wzp-client`, `wzp-client-audio`, `wzp-web`, `wzp-bench`.
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM rust:1.85 AS builder
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN cargo build --release --bin wzp-relay
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /src/target/release/wzp-relay /usr/local/bin/
|
||||
EXPOSE 4433/udp
|
||||
EXPOSE 9090/tcp
|
||||
VOLUME /data
|
||||
ENV HOME=/data
|
||||
ENTRYPOINT ["wzp-relay"]
|
||||
CMD ["--config", "/data/relay.toml", "--metrics-port", "9090"]
|
||||
```
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
docker build -t wzp-relay .
|
||||
docker run -d \
|
||||
--name wzp-relay \
|
||||
-p 4433:4433/udp \
|
||||
-p 9090:9090/tcp \
|
||||
-v /opt/wzp:/data \
|
||||
wzp-relay
|
||||
```
|
||||
|
||||
### systemd
|
||||
|
||||
Create `/etc/systemd/system/wzp-relay.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=WarzonePhone Relay
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=wzp
|
||||
Group=wzp
|
||||
ExecStart=/usr/local/bin/wzp-relay --config /etc/wzp/relay.toml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
LimitNOFILE=65536
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=yes
|
||||
ReadWritePaths=/var/lib/wzp
|
||||
PrivateTmp=yes
|
||||
|
||||
Environment=HOME=/var/lib/wzp
|
||||
Environment=RUST_LOG=info
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Setup:
|
||||
|
||||
```bash
|
||||
# Create service user
|
||||
useradd --system --home-dir /var/lib/wzp --create-home wzp
|
||||
|
||||
# Install binary and config
|
||||
cp target/release/wzp-relay /usr/local/bin/
|
||||
mkdir -p /etc/wzp
|
||||
cp relay.toml /etc/wzp/
|
||||
|
||||
# Enable and start
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now wzp-relay
|
||||
journalctl -u wzp-relay -f
|
||||
```
|
||||
|
||||
## TOML Configuration Reference
|
||||
|
||||
All fields have defaults. A minimal config file only needs the fields you want to override.
|
||||
|
||||
### Core Settings
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `listen_addr` | string (socket addr) | `"0.0.0.0:4433"` | UDP address to listen on for incoming QUIC connections |
|
||||
| `remote_relay` | string (socket addr) | none | Remote relay address for forward mode. Disables room mode when set |
|
||||
| `max_sessions` | integer | `100` | Maximum concurrent client sessions |
|
||||
| `log_level` | string | `"info"` | Logging level: trace, debug, info, warn, error |
|
||||
|
||||
### Jitter Buffer
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `jitter_target_depth` | integer | `50` | Target buffer depth in packets (50 = 1 second at 20ms frames) |
|
||||
| `jitter_max_depth` | integer | `250` | Maximum buffer depth in packets (250 = 5 seconds) |
|
||||
|
||||
### Authentication
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `auth_url` | string | none | featherChat auth validation URL. When set, clients must send a bearer token as their first signal message. The relay validates it via `POST <auth_url>` |
|
||||
|
||||
### Metrics and Monitoring
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `metrics_port` | integer | none | Port for the Prometheus HTTP metrics endpoint. Disabled if not set |
|
||||
| `probe_targets` | array of socket addrs | `[]` | Peer relay addresses to probe for health monitoring (1 Ping/s each) |
|
||||
| `probe_mesh` | boolean | `false` | Enable mesh mode for probe targets |
|
||||
|
||||
### Media Processing
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `trunking_enabled` | boolean | `false` | Enable trunk batching for outgoing media. Packs multiple session packets into one QUIC datagram, reducing overhead |
|
||||
|
||||
### WebSocket / Browser Support
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `ws_port` | integer | none | Port for WebSocket listener (browser clients). Disabled if not set |
|
||||
| `static_dir` | string | none | Directory to serve static files (HTML/JS/WASM) |
|
||||
|
||||
### Federation
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `peers` | array of PeerConfig | `[]` | Outbound federation peer relays |
|
||||
| `trusted` | array of TrustedConfig | `[]` | Inbound federation trust list |
|
||||
| `global_rooms` | array of GlobalRoomConfig | `[]` | Room names to bridge across federation |
|
||||
|
||||
### Debugging
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `debug_tap` | string | none | Log packet headers for matching rooms. Use `"*"` for all rooms, or a specific room name |
|
||||
|
||||
### PeerConfig Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `url` | string | yes | Address of the peer relay (e.g., `"193.180.213.68:4433"`) |
|
||||
| `fingerprint` | string | yes | Expected TLS certificate fingerprint (hex with colons) |
|
||||
| `label` | string | no | Human-readable label for logging |
|
||||
|
||||
### TrustedConfig Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `fingerprint` | string | yes | Expected TLS certificate fingerprint (hex with colons) |
|
||||
| `label` | string | no | Human-readable label for logging |
|
||||
|
||||
### GlobalRoomConfig Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `name` | string | yes | Room name to bridge across federation (e.g., `"android"`) |
|
||||
|
||||
## CLI Flags Reference
|
||||
|
||||
```
|
||||
wzp-relay [--config <path>] [--listen <addr>] [--remote <addr>]
|
||||
[--auth-url <url>] [--metrics-port <port>]
|
||||
[--probe <addr>]... [--probe-mesh] [--mesh-status]
|
||||
[--trunking] [--global-room <name>]...
|
||||
[--debug-tap <room>]
|
||||
[--ws-port <port>] [--static-dir <dir>]
|
||||
```
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--config <path>` | Load configuration from TOML file. CLI flags override config file values |
|
||||
| `--listen <addr>` | Listen address (default: `0.0.0.0:4433`) |
|
||||
| `--remote <addr>` | Remote relay for forwarding mode. Disables room mode |
|
||||
| `--auth-url <url>` | featherChat auth endpoint (e.g., `https://chat.example.com/v1/auth/validate`) |
|
||||
| `--metrics-port <port>` | Prometheus metrics HTTP port (e.g., `9090`) |
|
||||
| `--probe <addr>` | Peer relay to probe for health monitoring. Repeatable |
|
||||
| `--probe-mesh` | Enable mesh mode for probes |
|
||||
| `--mesh-status` | Print mesh health table and exit (diagnostic) |
|
||||
| `--trunking` | Enable trunk batching for outgoing media |
|
||||
| `--global-room <name>` | Declare a room as global (bridged across federation). Repeatable |
|
||||
| `--debug-tap <room>` | Log packet headers for a room (`"*"` for all rooms) |
|
||||
| `--ws-port <port>` | WebSocket listener port for browser clients |
|
||||
| `--static-dir <dir>` | Directory to serve static files from |
|
||||
| `--help`, `-h` | Print help and exit |
|
||||
|
||||
CLI flags always override config file values when both are specified.
|
||||
|
||||
## Federation Setup
|
||||
|
||||
### Concepts
|
||||
|
||||
- **`[[peers]]`** -- outbound: relays we connect TO. Requires address + fingerprint
|
||||
- **`[[trusted]]`** -- inbound: relays we accept connections FROM. Requires fingerprint only (they connect to us)
|
||||
- **`[[global_rooms]]`** -- rooms bridged across all federated peers. Participants on different relays in the same global room hear each other
|
||||
|
||||
### Getting Your Relay's Fingerprint
|
||||
|
||||
When a relay starts, it logs its TLS fingerprint:
|
||||
|
||||
```
|
||||
INFO TLS certificate (deterministic from relay identity) tls_fingerprint="a5d6:e3c6:5ae7:185c:4eb1:af89:daed:4a43"
|
||||
INFO federation: to peer with this relay, add to relay.toml:
|
||||
INFO [[peers]]
|
||||
INFO url = "193.180.213.68:4433"
|
||||
INFO fingerprint = "a5d6:e3c6:5ae7:185c:4eb1:af89:daed:4a43"
|
||||
```
|
||||
|
||||
Share this information with the administrator of the peer relay.
|
||||
|
||||
### Unknown Peer Connections
|
||||
|
||||
When an unknown relay tries to federate, the log shows:
|
||||
|
||||
```
|
||||
WARN unknown relay wants to federate addr=10.0.0.5:12345 fp="7f2a:b391:0c44:..."
|
||||
INFO to accept, add to relay.toml:
|
||||
INFO [[trusted]]
|
||||
INFO fingerprint = "7f2a:b391:0c44:..."
|
||||
INFO label = "Relay at 10.0.0.5:12345"
|
||||
```
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Single Relay (Minimal)
|
||||
|
||||
```toml
|
||||
# /etc/wzp/relay.toml
|
||||
# Minimal config -- all defaults, just enable metrics
|
||||
metrics_port = 9090
|
||||
```
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
wzp-relay --config /etc/wzp/relay.toml
|
||||
```
|
||||
|
||||
### Single Relay (Full Featured)
|
||||
|
||||
```toml
|
||||
# /etc/wzp/relay.toml
|
||||
listen_addr = "0.0.0.0:4433"
|
||||
max_sessions = 200
|
||||
log_level = "info"
|
||||
|
||||
# Metrics
|
||||
metrics_port = 9090
|
||||
|
||||
# Authentication
|
||||
auth_url = "https://chat.example.com/v1/auth/validate"
|
||||
|
||||
# Browser support
|
||||
ws_port = 8080
|
||||
static_dir = "/opt/wzp/web"
|
||||
|
||||
# Performance
|
||||
trunking_enabled = true
|
||||
|
||||
# Jitter buffer tuning
|
||||
jitter_target_depth = 50
|
||||
jitter_max_depth = 250
|
||||
```
|
||||
|
||||
### Two-Relay Federation
|
||||
|
||||
**Relay A** (`relay-a.toml` on 193.180.213.68):
|
||||
|
||||
```toml
|
||||
listen_addr = "0.0.0.0:4433"
|
||||
metrics_port = 9090
|
||||
|
||||
# Outbound: connect to Relay B
|
||||
[[peers]]
|
||||
url = "10.0.0.5:4433"
|
||||
fingerprint = "7f2a:b391:0c44:9e1d:a8b2:c5d7:e3f0:1234"
|
||||
label = "Relay B (US)"
|
||||
|
||||
# Accept inbound from Relay B
|
||||
[[trusted]]
|
||||
fingerprint = "7f2a:b391:0c44:9e1d:a8b2:c5d7:e3f0:1234"
|
||||
label = "Relay B (US)"
|
||||
|
||||
# Bridge these rooms
|
||||
[[global_rooms]]
|
||||
name = "android"
|
||||
|
||||
[[global_rooms]]
|
||||
name = "general"
|
||||
```
|
||||
|
||||
**Relay B** (`relay-b.toml` on 10.0.0.5):
|
||||
|
||||
```toml
|
||||
listen_addr = "0.0.0.0:4433"
|
||||
metrics_port = 9090
|
||||
|
||||
# Outbound: connect to Relay A
|
||||
[[peers]]
|
||||
url = "193.180.213.68:4433"
|
||||
fingerprint = "a5d6:e3c6:5ae7:185c:4eb1:af89:daed:4a43"
|
||||
label = "Relay A (EU)"
|
||||
|
||||
# Accept inbound from Relay A
|
||||
[[trusted]]
|
||||
fingerprint = "a5d6:e3c6:5ae7:185c:4eb1:af89:daed:4a43"
|
||||
label = "Relay A (EU)"
|
||||
|
||||
# Same global rooms
|
||||
[[global_rooms]]
|
||||
name = "android"
|
||||
|
||||
[[global_rooms]]
|
||||
name = "general"
|
||||
```
|
||||
|
||||
### Three-Relay Chain (Full Mesh)
|
||||
|
||||
For three relays (A, B, C) in full mesh federation, each relay needs peers and trusted entries for the other two:
|
||||
|
||||
**Relay A** (EU):
|
||||
|
||||
```toml
|
||||
listen_addr = "0.0.0.0:4433"
|
||||
metrics_port = 9090
|
||||
|
||||
# Probe all peers
|
||||
probe_targets = ["10.0.0.5:4433", "10.0.0.9:4433"]
|
||||
probe_mesh = true
|
||||
|
||||
# Peers
|
||||
[[peers]]
|
||||
url = "10.0.0.5:4433"
|
||||
fingerprint = "7f2a:b391:0c44:9e1d:a8b2:c5d7:e3f0:1234"
|
||||
label = "Relay B (US)"
|
||||
|
||||
[[peers]]
|
||||
url = "10.0.0.9:4433"
|
||||
fingerprint = "3c8e:d2a1:f7b5:6049:81c3:e9d4:a2f6:5678"
|
||||
label = "Relay C (APAC)"
|
||||
|
||||
# Trust
|
||||
[[trusted]]
|
||||
fingerprint = "7f2a:b391:0c44:9e1d:a8b2:c5d7:e3f0:1234"
|
||||
label = "Relay B (US)"
|
||||
|
||||
[[trusted]]
|
||||
fingerprint = "3c8e:d2a1:f7b5:6049:81c3:e9d4:a2f6:5678"
|
||||
label = "Relay C (APAC)"
|
||||
|
||||
# Global rooms
|
||||
[[global_rooms]]
|
||||
name = "android"
|
||||
|
||||
[[global_rooms]]
|
||||
name = "general"
|
||||
```
|
||||
|
||||
**Relay B** and **Relay C** follow the same pattern, listing the other two relays in their `[[peers]]` and `[[trusted]]` sections.
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
Enable with `--metrics-port <port>` or `metrics_port` in TOML. The relay exposes metrics at `GET /metrics` on the specified HTTP port.
|
||||
|
||||
#### Relay Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `wzp_relay_active_sessions` | Gauge | -- | Current active sessions |
|
||||
| `wzp_relay_active_rooms` | Gauge | -- | Current active rooms |
|
||||
| `wzp_relay_packets_forwarded_total` | Counter | `room` | Total packets forwarded |
|
||||
| `wzp_relay_bytes_forwarded_total` | Counter | `room` | Total bytes forwarded |
|
||||
| `wzp_relay_auth_attempts_total` | Counter | `result` (ok/fail) | Auth validation attempts |
|
||||
| `wzp_relay_handshake_duration_seconds` | Histogram | -- | Crypto handshake time |
|
||||
|
||||
#### Per-Session Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `wzp_relay_session_jitter_buffer_depth` | Gauge | `session_id` | Buffer depth per session |
|
||||
| `wzp_relay_session_loss_pct` | Gauge | `session_id` | Packet loss percentage |
|
||||
| `wzp_relay_session_rtt_ms` | Gauge | `session_id` | Round-trip time |
|
||||
| `wzp_relay_session_underruns_total` | Counter | `session_id` | Jitter buffer underruns |
|
||||
| `wzp_relay_session_overruns_total` | Counter | `session_id` | Jitter buffer overruns |
|
||||
|
||||
#### Inter-Relay Probe Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `wzp_probe_rtt_ms` | Gauge | `target` | RTT to peer relay |
|
||||
| `wzp_probe_loss_pct` | Gauge | `target` | Loss to peer relay |
|
||||
| `wzp_probe_jitter_ms` | Gauge | `target` | Jitter to peer relay |
|
||||
| `wzp_probe_up` | Gauge | `target` | 1 if reachable, 0 if not |
|
||||
|
||||
### Prometheus Scrape Config
|
||||
|
||||
```yaml
|
||||
# prometheus.yml
|
||||
scrape_configs:
|
||||
- job_name: 'wzp-relay'
|
||||
static_configs:
|
||||
- targets:
|
||||
- 'relay-a:9090'
|
||||
- 'relay-b:9090'
|
||||
scrape_interval: 10s
|
||||
```
|
||||
|
||||
### Grafana Dashboard
|
||||
|
||||
A pre-built dashboard is available at `docs/grafana-dashboard.json`. Import it into Grafana for:
|
||||
|
||||
1. **Relay Health** -- active sessions, rooms, packets/s, bytes/s
|
||||
2. **Call Quality** -- per-session jitter depth, loss%, RTT, underruns over time
|
||||
3. **Inter-Relay Mesh** -- latency heatmap, probe status, loss trends
|
||||
4. **Web Bridge** -- active connections, frames bridged, auth failures
|
||||
|
||||
### Debug Tap
|
||||
|
||||
Use `--debug-tap` to log packet headers for debugging:
|
||||
|
||||
```bash
|
||||
# Log headers for room "android"
|
||||
wzp-relay --debug-tap android
|
||||
|
||||
# Log headers for all rooms
|
||||
wzp-relay --debug-tap '*'
|
||||
```
|
||||
|
||||
Or in TOML:
|
||||
|
||||
```toml
|
||||
debug_tap = "android"
|
||||
```
|
||||
|
||||
### Mesh Status
|
||||
|
||||
Print the current mesh health table (diagnostic):
|
||||
|
||||
```bash
|
||||
wzp-relay --mesh-status
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### featherChat Token Validation
|
||||
|
||||
When `--auth-url` is set, the relay requires clients to send an `AuthToken` signal message as their first message after QUIC connection. The relay validates the token by calling:
|
||||
|
||||
```
|
||||
POST <auth_url>
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Expected response:
|
||||
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"fingerprint": "a5d6:e3c6:...",
|
||||
"alias": "username"
|
||||
}
|
||||
```
|
||||
|
||||
If validation fails, the client is disconnected.
|
||||
|
||||
### Without Authentication
|
||||
|
||||
When `--auth-url` is not set, any client can connect. The relay logs:
|
||||
|
||||
```
|
||||
INFO auth disabled -- any client can connect (use --auth-url to enable)
|
||||
```
|
||||
|
||||
## Identity Persistence
|
||||
|
||||
### Relay Identity File
|
||||
|
||||
The relay stores its identity seed at `~/.wzp/relay-identity` (a 64-character hex string). This seed:
|
||||
|
||||
- Is generated automatically on first run
|
||||
- Persists across restarts
|
||||
- Derives the relay's Ed25519 signing key and X25519 key agreement key
|
||||
- Derives the TLS certificate deterministically (same seed = same cert = same fingerprint)
|
||||
|
||||
If the identity file is corrupted, the relay generates a new one and logs a warning. This will change the relay's TLS fingerprint, requiring federation peers to update their config.
|
||||
|
||||
### Backup
|
||||
|
||||
Back up the identity file to preserve the relay's fingerprint:
|
||||
|
||||
```bash
|
||||
cp ~/.wzp/relay-identity /secure/backup/relay-identity
|
||||
```
|
||||
|
||||
To restore, copy the file back before starting the relay.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Problem | Cause | Solution |
|
||||
|---------|-------|---------|
|
||||
| "unknown argument" on startup | Unrecognized CLI flag | Check `wzp-relay --help` for valid flags |
|
||||
| "failed to load config" | Invalid TOML syntax | Validate TOML file with `toml-cli` or similar |
|
||||
| "auth failed" for all clients | Wrong `auth_url` or featherChat server down | Verify URL is reachable: `curl -X POST <auth_url>` |
|
||||
| "session rejected" | Max sessions reached | Increase `max_sessions` in config |
|
||||
| Clients cannot connect | Firewall blocking UDP 4433 | Open UDP port 4433 in firewall |
|
||||
| Federation "unknown relay wants to federate" | Peer's fingerprint not in `[[trusted]]` | Add the logged fingerprint to `[[trusted]]` |
|
||||
| Federation "fingerprint mismatch" | Peer relay restarted with new identity | Update the fingerprint in `[[peers]]` config |
|
||||
| High packet loss between relays | Network congestion or misconfiguration | Check `wzp_probe_loss_pct` metric; consider relay chaining |
|
||||
| Jitter buffer overruns | Packets arriving faster than playout | Increase `jitter_max_depth` |
|
||||
| Jitter buffer underruns | Packets arriving too slowly or lost | Check network quality; increase `jitter_target_depth` |
|
||||
| "probe connection closed" | Peer relay unreachable or crashed | Check peer relay status; will auto-reconnect |
|
||||
| WebSocket clients cannot connect | `ws_port` not set | Add `--ws-port <port>` or `ws_port` in TOML |
|
||||
| Browser mic access denied | Not using HTTPS | Use TLS termination in front of the relay or serve via `wzp-web --tls` |
|
||||
|
||||
### Log Level Tuning
|
||||
|
||||
Set `RUST_LOG` environment variable for fine-grained control:
|
||||
|
||||
```bash
|
||||
# All relay logs at debug level
|
||||
RUST_LOG=debug wzp-relay
|
||||
|
||||
# Only federation at trace, everything else at info
|
||||
RUST_LOG=info,wzp_relay::federation=trace wzp-relay
|
||||
|
||||
# Quiet mode -- only warnings and errors
|
||||
RUST_LOG=warn wzp-relay
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```bash
|
||||
# Check if relay is listening
|
||||
nc -zu relay-host 4433
|
||||
|
||||
# Check metrics endpoint
|
||||
curl -s http://relay-host:9090/metrics | head -20
|
||||
|
||||
# Check active sessions
|
||||
curl -s http://relay-host:9090/metrics | grep wzp_relay_active_sessions
|
||||
|
||||
# Check federation probe health
|
||||
curl -s http://relay-host:9090/metrics | grep wzp_probe_up
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
665
docs/DESIGN.md
665
docs/DESIGN.md
@@ -1,168 +1,591 @@
|
||||
# WarzonePhone Detailed Design Decisions
|
||||
# WarzonePhone Design Document
|
||||
|
||||
## Why Opus + Codec2 (Not Just One)
|
||||
> Custom encrypted VoIP protocol built in Rust. Designed for hostile network conditions: 5-70% packet loss, 100-500 kbps throughput, 300-800 ms RTT. Multi-platform: Desktop (Tauri), Android, CLI, Web.
|
||||
|
||||
The dual-codec architecture is driven by the extreme range of network conditions WarzonePhone targets:
|
||||
## System Overview
|
||||
|
||||
**Opus** (24/16/6 kbps) is the clear choice for normal to degraded conditions. It offers excellent quality at moderate bitrates, has built-in inband FEC and DTX (discontinuous transmission), and the `audiopus` crate provides mature Rust bindings to libopus. Opus operates at 48 kHz natively.
|
||||
WarzonePhone is a voice-over-IP system built from scratch in Rust, targeting reliable encrypted voice communication over severely degraded networks. The protocol uses adaptive codecs (Opus + Codec2), fountain-code FEC (RaptorQ), and end-to-end ChaCha20-Poly1305 encryption over a QUIC transport layer.
|
||||
|
||||
**Codec2** (3200/1200 bps) is a narrowband vocoder designed specifically for HF radio links with extreme bandwidth constraints. At 1200 bps (1.2 kbps), it produces intelligible speech in only 6 bytes per 40ms frame -- roughly 20x lower bitrate than Opus at its minimum. The pure-Rust `codec2` crate means no C dependencies for this codec. Codec2 operates at 8 kHz, so the adaptive layer handles 48 kHz <-> 8 kHz resampling transparently.
|
||||
The system comprises three categories of components:
|
||||
|
||||
The `AdaptiveEncoder`/`AdaptiveDecoder` in `crates/wzp-codec/src/adaptive.rs` hold both codec instances and switch between them based on the active `QualityProfile`. This avoids codec re-initialization latency during tier transitions.
|
||||
1. **Protocol crates** -- a Rust workspace of 7 library crates with a star dependency graph enabling parallel development
|
||||
2. **Client applications** -- Desktop (Tauri), Android (Kotlin + JNI), CLI, and Web (browser bridge)
|
||||
3. **Relay infrastructure** -- SFU relay daemons with federation, health probing, and Prometheus metrics
|
||||
|
||||
**Bandwidth comparison with FEC overhead:**
|
||||
### Design Principles
|
||||
|
||||
| Tier | Codec Bitrate | FEC Ratio | Total Bandwidth |
|
||||
|------|--------------|-----------|----------------|
|
||||
| GOOD | 24 kbps | 20% | ~28.8 kbps |
|
||||
| DEGRADED | 6 kbps | 50% | ~9.0 kbps |
|
||||
| CATASTROPHIC | 1.2 kbps | 100% | ~2.4 kbps |
|
||||
- **User sovereignty** -- client-driven route selection, BIP39 identity backup, no central authority
|
||||
- **End-to-end encryption** -- relays never see plaintext audio; SFU forwarding preserves E2E encryption
|
||||
- **Adaptive resilience** -- automatic codec and FEC switching based on observed network quality
|
||||
- **Parallel development** -- star dependency graph allows 5 agents/developers to work simultaneously with zero merge conflicts
|
||||
|
||||
At the catastrophic tier, the entire call (audio + FEC + headers) fits within approximately 3 kbps, which is viable even over severely degraded links.
|
||||
## Architecture
|
||||
|
||||
## Why RaptorQ Over Reed-Solomon
|
||||
### Crate Overview
|
||||
|
||||
**Reed-Solomon** is a classical block erasure code. It works well but has fixed-rate overhead: you must decide in advance how many repair symbols to generate, and decoding requires receiving exactly K of any K+R symbols.
|
||||
The workspace contains 7 core crates plus integration binaries:
|
||||
|
||||
**RaptorQ** (RFC 6330) is a fountain code with several advantages for VoIP:
|
||||
| Crate | Purpose | Key Dependencies |
|
||||
|-------|---------|-----------------|
|
||||
| `wzp-proto` | Protocol types, traits, wire format | serde, bytes |
|
||||
| `wzp-codec` | Audio codecs (Opus, Codec2, RNNoise) | audiopus, codec2, nnnoiseless |
|
||||
| `wzp-fec` | Forward error correction | raptorq |
|
||||
| `wzp-crypto` | Cryptography and identity | ed25519-dalek, x25519-dalek, chacha20poly1305, bip39 |
|
||||
| `wzp-transport` | QUIC transport layer | quinn, rustls |
|
||||
| `wzp-relay` | Relay daemon (SFU, federation, metrics) | tokio, prometheus |
|
||||
| `wzp-client` | Call engine and CLI | All above |
|
||||
|
||||
1. **Rateless**: You can generate an arbitrary number of repair symbols on the fly. If conditions worsen mid-block, you can generate additional repair without re-encoding.
|
||||
Additional integration targets: `wzp-web` (browser bridge via WebSocket), Android native library (JNI), Desktop (Tauri).
|
||||
|
||||
2. **Efficient decoding**: RaptorQ can decode from any K symbols with high probability (typically K + 1 or K + 2 suffice), compared to Reed-Solomon which requires exactly K.
|
||||
### Dependency Graph
|
||||
|
||||
3. **Lower computational complexity**: O(K) encoding and decoding time, compared to O(K^2) for Reed-Solomon. This matters for real-time audio at 50 frames/second.
|
||||
```mermaid
|
||||
graph TD
|
||||
PROTO["wzp-proto<br/>(Types, Traits, Wire Format)"]
|
||||
|
||||
4. **Variable block sizes**: The encoder handles 1-56403 source symbols per block (the WZP implementation uses 5-10, but the flexibility is there).
|
||||
CODEC["wzp-codec<br/>(Opus + Codec2 + RNNoise)"]
|
||||
FEC["wzp-fec<br/>(RaptorQ FEC)"]
|
||||
CRYPTO["wzp-crypto<br/>(ChaCha20 + Identity)"]
|
||||
TRANSPORT["wzp-transport<br/>(QUIC / Quinn)"]
|
||||
|
||||
The `raptorq` crate (v2) provides a well-tested pure-Rust implementation. The WZP FEC layer adds length-prefixed padding (2-byte LE prefix + zero-pad to 256 bytes) so that variable-length audio frames can be recovered exactly.
|
||||
RELAY["wzp-relay<br/>(Relay Daemon)"]
|
||||
CLIENT["wzp-client<br/>(CLI + Call Engine)"]
|
||||
WEB["wzp-web<br/>(Browser Bridge)"]
|
||||
DESKTOP["Desktop<br/>(Tauri + CPAL)"]
|
||||
ANDROID["Android<br/>(Kotlin + JNI)"]
|
||||
|
||||
**FEC bandwidth math at different loss rates:**
|
||||
PROTO --> CODEC
|
||||
PROTO --> FEC
|
||||
PROTO --> CRYPTO
|
||||
PROTO --> TRANSPORT
|
||||
|
||||
CODEC --> CLIENT
|
||||
FEC --> CLIENT
|
||||
CRYPTO --> CLIENT
|
||||
TRANSPORT --> CLIENT
|
||||
|
||||
CODEC --> RELAY
|
||||
FEC --> RELAY
|
||||
CRYPTO --> RELAY
|
||||
TRANSPORT --> RELAY
|
||||
|
||||
CLIENT --> WEB
|
||||
CLIENT --> DESKTOP
|
||||
CLIENT --> ANDROID
|
||||
TRANSPORT --> WEB
|
||||
|
||||
FC["warzone-protocol<br/>(featherChat Identity)"] -.->|path dep| CRYPTO
|
||||
|
||||
style PROTO fill:#6c5ce7,color:#fff
|
||||
style RELAY fill:#ff9f43,color:#fff
|
||||
style CLIENT fill:#00b894,color:#fff
|
||||
style WEB fill:#0984e3,color:#fff
|
||||
style DESKTOP fill:#0984e3,color:#fff
|
||||
style ANDROID fill:#0984e3,color:#fff
|
||||
style FC fill:#fd79a8,color:#fff
|
||||
```
|
||||
|
||||
The star pattern ensures each leaf crate (`wzp-codec`, `wzp-fec`, `wzp-crypto`, `wzp-transport`) depends only on `wzp-proto` and never on each other. This enables:
|
||||
|
||||
- **Parallel development** -- 5 agents work on 5 crates with no merge conflicts
|
||||
- **Independent testing** -- each crate has self-contained tests
|
||||
- **Pluggability** -- any implementation can be swapped by implementing the same trait
|
||||
- **Fast compilation** -- changing one leaf only recompiles that leaf and integration crates
|
||||
|
||||
## Audio Pipeline
|
||||
|
||||
### Encode Pipeline (Mic to Network)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Mic as Microphone
|
||||
participant RNN as RNNoise Denoise
|
||||
participant VAD as Silence Detector
|
||||
participant ENC as Opus/Codec2 Encode
|
||||
participant FEC as RaptorQ FEC Encode
|
||||
participant INT as Interleaver
|
||||
participant HDR as Header Assembly
|
||||
participant CRYPT as ChaCha20-Poly1305
|
||||
participant QUIC as QUIC Datagram
|
||||
|
||||
Mic->>RNN: PCM i16 x 960 (20ms @ 48kHz)
|
||||
RNN->>VAD: Denoised samples (2 x 480)
|
||||
alt Silence detected (>100ms)
|
||||
VAD->>ENC: ComfortNoise packet (every 200ms)
|
||||
else Active speech or hangover
|
||||
VAD->>ENC: Active audio frame
|
||||
end
|
||||
ENC->>FEC: Compressed frame (padded to 256 bytes)
|
||||
FEC->>FEC: Accumulate block (5-10 frames)
|
||||
FEC->>INT: Source + repair symbols
|
||||
INT->>HDR: Interleaved packets (depth=3)
|
||||
HDR->>CRYPT: MediaHeader (12B) or MiniHeader (4B)
|
||||
CRYPT->>QUIC: Header=AAD, Payload=encrypted
|
||||
```
|
||||
|
||||
### Decode Pipeline (Network to Speaker)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant QUIC as QUIC Datagram
|
||||
participant CRYPT as ChaCha20-Poly1305
|
||||
participant HDR as Header Parse
|
||||
participant DEINT as De-interleaver
|
||||
participant FEC as RaptorQ FEC Decode
|
||||
participant JIT as Jitter Buffer
|
||||
participant DEC as Opus/Codec2 Decode
|
||||
participant SPK as Speaker
|
||||
|
||||
QUIC->>CRYPT: Encrypted packet
|
||||
CRYPT->>HDR: Decrypt (header=AAD)
|
||||
HDR->>DEINT: Parsed MediaHeader + payload
|
||||
DEINT->>FEC: Reordered symbols
|
||||
FEC->>FEC: Reconstruct from any K of K+R symbols
|
||||
FEC->>JIT: Recovered audio frames
|
||||
JIT->>JIT: Sequence-ordered BTreeMap
|
||||
JIT->>DEC: Pop when depth >= target
|
||||
DEC->>SPK: PCM i16 x 960
|
||||
```
|
||||
|
||||
## Codec System
|
||||
|
||||
WarzonePhone uses a dual-codec architecture to cover the full range of network conditions:
|
||||
|
||||
### Opus (Primary)
|
||||
|
||||
Opus is the primary codec for normal to degraded conditions. It operates at 48 kHz natively with built-in inband FEC and DTX (discontinuous transmission). The `audiopus` crate provides mature Rust bindings to libopus.
|
||||
|
||||
| Profile | Bitrate | Frame Duration | FEC Ratio | Total Bandwidth | Use Case |
|
||||
|---------|---------|---------------|-----------|----------------|----------|
|
||||
| Studio 64k | 64 kbps | 20ms | 10% | 70.4 kbps | LAN, excellent WiFi |
|
||||
| Studio 48k | 48 kbps | 20ms | 10% | 52.8 kbps | Good WiFi, wired |
|
||||
| Studio 32k | 32 kbps | 20ms | 10% | 35.2 kbps | WiFi, LTE |
|
||||
| Good (24k) | 24 kbps | 20ms | 20% | 28.8 kbps | WiFi, LTE, decent links |
|
||||
| Opus 16k | 16 kbps | 20ms | 20% | 19.2 kbps | 3G, moderate congestion |
|
||||
| Degraded (6k) | 6 kbps | 40ms | 50% | 9.0 kbps | 3G, congested WiFi |
|
||||
|
||||
### Codec2 (Fallback)
|
||||
|
||||
Codec2 is a narrowband vocoder designed for HF radio links with extreme bandwidth constraints. It operates at 8 kHz, and the adaptive layer handles 48 kHz <-> 8 kHz resampling transparently. The pure-Rust `codec2` crate means no C dependencies.
|
||||
|
||||
| Profile | Bitrate | Frame Duration | FEC Ratio | Total Bandwidth | Use Case |
|
||||
|---------|---------|---------------|-----------|----------------|----------|
|
||||
| Codec2 3200 | 3.2 kbps | 20ms | 50% | 4.8 kbps | Poor conditions |
|
||||
| Catastrophic (1200) | 1.2 kbps | 40ms | 100% | 2.4 kbps | Satellite, extreme loss |
|
||||
|
||||
### ComfortNoise
|
||||
|
||||
When the silence detector identifies no speech activity for over 100ms, the encoder switches to emitting a ComfortNoise packet every 200ms instead of encoding silence. This provides approximately 50% bandwidth savings in typical conversations.
|
||||
|
||||
### Adaptive Switching
|
||||
|
||||
The `AdaptiveEncoder`/`AdaptiveDecoder` in `wzp-codec` hold both codec instances and switch between them based on the active `QualityProfile`. This avoids codec re-initialization latency during tier transitions. The `AdaptiveQualityController` in `wzp-proto` manages tier transitions with hysteresis:
|
||||
|
||||
- **Downgrade**: 3 consecutive bad reports (2 on cellular networks)
|
||||
- **Upgrade**: 10 consecutive good reports (one tier at a time)
|
||||
- **Network handoff**: WiFi-to-cellular switch triggers preemptive one-tier downgrade plus a temporary 10-second FEC boost (+20%)
|
||||
|
||||
Quality tier classification thresholds:
|
||||
|
||||
| Tier | WiFi/Unknown | Cellular |
|
||||
|------|-------------|----------|
|
||||
| Good | loss < 10%, RTT < 400ms | loss < 8%, RTT < 300ms |
|
||||
| Degraded | loss 10-40%, RTT 400-600ms | loss 8-25%, RTT 300-500ms |
|
||||
| Catastrophic | loss > 40%, RTT > 600ms | loss > 25%, RTT > 500ms |
|
||||
|
||||
## Forward Error Correction (FEC)
|
||||
|
||||
### Why RaptorQ Over Reed-Solomon
|
||||
|
||||
WarzonePhone uses RaptorQ (RFC 6330) fountain codes via the `raptorq` crate:
|
||||
|
||||
1. **Rateless** -- generate arbitrary repair symbols on the fly; if conditions worsen mid-block, generate additional repair without re-encoding
|
||||
2. **Efficient decoding** -- decode from any K symbols with high probability (typically K + 1 or K + 2 suffice)
|
||||
3. **Lower complexity** -- O(K) encoding/decoding time vs O(K^2) for Reed-Solomon
|
||||
4. **Variable block sizes** -- 1-56,403 source symbols per block (WZP uses 5-10)
|
||||
|
||||
### FEC Block Structure
|
||||
|
||||
Each FEC block consists of 5-10 audio frames padded to 256-byte symbols with a 2-byte LE length prefix:
|
||||
|
||||
```
|
||||
[len:u16 LE][audio_frame][zero_padding_to_256_bytes]
|
||||
```
|
||||
|
||||
### Loss Survival by FEC Ratio
|
||||
|
||||
With 5 source frames per block:
|
||||
- 20% repair (GOOD): 1 repair symbol. Survives loss of 1 out of 6 packets (16.7% loss).
|
||||
- 50% repair (DEGRADED): 3 repair symbols. Survives loss of 3 out of 8 packets (37.5% loss).
|
||||
- 100% repair (CATASTROPHIC): 5 repair symbols. Survives loss of 5 out of 10 packets (50% loss).
|
||||
|
||||
The benchmark (`wzp-bench --fec --loss 30`) dynamically scales the FEC ratio to survive the requested loss percentage.
|
||||
| FEC Ratio | Repair Symbols | Survives Loss | Profile |
|
||||
|-----------|---------------|---------------|---------|
|
||||
| 10% | 1 | 1 of 6 (16.7%) | Studio |
|
||||
| 20% | 1 | 1 of 6 (16.7%) | Good |
|
||||
| 50% | 3 | 3 of 8 (37.5%) | Degraded |
|
||||
| 100% | 5 | 5 of 10 (50.0%) | Catastrophic |
|
||||
|
||||
## Why QUIC Over Raw UDP
|
||||
### Interleaving
|
||||
|
||||
Raw UDP would be simpler and lower-latency, but QUIC (via the `quinn` crate) provides:
|
||||
Burst loss protection via depth-3 interleaving: packets from 3 consecutive FEC blocks are interleaved before transmission. A burst of 3 consecutive lost packets affects 3 different blocks (1 loss each) rather than destroying 1 block entirely.
|
||||
|
||||
1. **DATAGRAM frames**: Unreliable delivery without head-of-line blocking (RFC 9221). Media packets use this path, so they behave like UDP datagrams but benefit from QUIC's connection management.
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph "FEC Encoder"
|
||||
F1[Frame 1] --> BLK[Source Block<br/>5-10 frames]
|
||||
F2[Frame 2] --> BLK
|
||||
F3[Frame 3] --> BLK
|
||||
F4[Frame 4] --> BLK
|
||||
F5[Frame 5] --> BLK
|
||||
BLK --> SRC[Source Symbols]
|
||||
BLK --> REP[Repair Symbols<br/>ratio-dependent]
|
||||
SRC --> INT[Interleaver<br/>depth=3]
|
||||
REP --> INT
|
||||
end
|
||||
|
||||
2. **Reliable streams**: Signaling messages (CallOffer, CallAnswer, Rekey, Hangup) require reliable delivery. QUIC provides multiplexed streams without needing a separate TCP connection.
|
||||
subgraph "Network"
|
||||
INT --> LOSS{Packet Loss}
|
||||
LOSS -->|some lost| RCV[Received Symbols]
|
||||
end
|
||||
|
||||
3. **Built-in congestion control**: QUIC's congestion control prevents overwhelming degraded links, which is important when chaining relays.
|
||||
subgraph "FEC Decoder"
|
||||
RCV --> DEINT[De-interleaver]
|
||||
DEINT --> RAPTORQ[RaptorQ Decode<br/>Any K of K+R]
|
||||
RAPTORQ --> OUT[Original Frames]
|
||||
end
|
||||
|
||||
4. **Connection migration**: QUIC connections survive IP address changes (e.g., WiFi to cellular handoff), which is valuable for mobile clients.
|
||||
|
||||
5. **TLS 1.3 built-in**: The QUIC handshake provides encryption at the transport level. While WZP has its own end-to-end ChaCha20 layer, the QUIC TLS protects the header and signaling from eavesdroppers.
|
||||
|
||||
6. **NAT keepalive**: QUIC's built-in keep-alive (configured at 5-second intervals) maintains NAT bindings without application-level pings.
|
||||
|
||||
7. **Firewall traversal**: QUIC runs on UDP port 443 by default, which is commonly allowed through firewalls. The `wzp` ALPN protocol identifier distinguishes WZP traffic.
|
||||
|
||||
The tradeoff is approximately 20-40 bytes of additional per-packet overhead compared to raw UDP (QUIC short header + DATAGRAM frame overhead).
|
||||
|
||||
## Why ChaCha20-Poly1305 Over AES-GCM
|
||||
|
||||
1. **Software performance**: ChaCha20-Poly1305 is faster than AES-GCM on hardware without AES-NI instructions. This matters for ARM devices (Android phones, Raspberry Pi relays, embedded systems) where AES hardware acceleration may be absent.
|
||||
|
||||
2. **Constant-time by design**: ChaCha20 uses only add-rotate-XOR operations, making it inherently resistant to timing side-channel attacks. AES-GCM implementations without hardware support often require careful constant-time implementation.
|
||||
|
||||
3. **Warzone messenger compatibility**: The existing Warzone messenger uses ChaCha20-Poly1305 for message encryption. Reusing the same primitive simplifies the security audit and allows key material to be shared across messaging and calling.
|
||||
|
||||
4. **16-byte overhead**: Both ChaCha20-Poly1305 and AES-128-GCM produce a 16-byte authentication tag. There is no size advantage to AES-GCM.
|
||||
|
||||
5. **AEAD with AAD**: The MediaHeader is used as Associated Authenticated Data (AAD), ensuring the header is authenticated but not encrypted. This allows relays to read routing information (block ID, sequence number) without decrypting the payload.
|
||||
|
||||
## Why Star Dependency Graph (Parallel Development)
|
||||
|
||||
The workspace follows a strict star dependency pattern:
|
||||
|
||||
```
|
||||
wzp-proto (hub)
|
||||
/ | \ \
|
||||
wzp-codec wzp-fec wzp-crypto wzp-transport
|
||||
\ | / /
|
||||
wzp-relay
|
||||
wzp-client
|
||||
wzp-web
|
||||
style LOSS fill:#e17055,color:#fff
|
||||
style RAPTORQ fill:#00b894,color:#fff
|
||||
```
|
||||
|
||||
- `wzp-proto` defines all trait interfaces and wire format types
|
||||
- Each "leaf" crate (codec, fec, crypto, transport) depends only on `wzp-proto`
|
||||
- No leaf crate depends on another leaf crate
|
||||
- Integration crates (relay, client, web) depend on all leaves
|
||||
## Transport Layer
|
||||
|
||||
This enables:
|
||||
1. **Parallel development**: 5 agents/developers can work on 5 crates simultaneously with zero merge conflicts
|
||||
2. **Independent testing**: Each crate has comprehensive tests that run without requiring other implementations
|
||||
3. **Pluggability**: Any implementation can be swapped (e.g., replace RaptorQ with Reed-Solomon) by implementing the same trait
|
||||
4. **Fast compilation**: Changes to one leaf only recompile that leaf and the integration crates, not other leaves
|
||||
### Why QUIC Over Raw UDP
|
||||
|
||||
## Jitter Buffer Trade-offs
|
||||
WarzonePhone uses QUIC (via the `quinn` crate) rather than raw UDP for several reasons:
|
||||
|
||||
The jitter buffer must balance two competing goals:
|
||||
| Feature | Benefit |
|
||||
|---------|---------|
|
||||
| DATAGRAM frames (RFC 9221) | Unreliable delivery without head-of-line blocking -- behaves like UDP for media |
|
||||
| Reliable streams | Multiplexed signaling (CallOffer, Hangup, Rekey) without a separate TCP connection |
|
||||
| Congestion control | Prevents overwhelming degraded links, important when chaining relays |
|
||||
| Connection migration | Connections survive IP address changes (WiFi to cellular handoff) |
|
||||
| TLS 1.3 built-in | Transport-level encryption protects headers and signaling |
|
||||
| NAT keepalive | 5-second interval maintains NAT bindings without application-level pings |
|
||||
| Firewall traversal | Runs on UDP port 443 with `wzp` ALPN identifier |
|
||||
|
||||
**Lower latency** (smaller buffer):
|
||||
- Better conversational interactivity
|
||||
- Less memory usage
|
||||
- But more vulnerable to jitter and reordering
|
||||
The tradeoff is approximately 20-40 bytes of additional per-packet overhead compared to raw UDP.
|
||||
|
||||
**Higher quality** (larger buffer):
|
||||
- More time to receive out-of-order packets
|
||||
- More time for FEC recovery (repair packets may arrive after source packets)
|
||||
- But adds perceptible delay to the conversation
|
||||
### Wire Formats
|
||||
|
||||
The default configuration:
|
||||
- Target: 10 packets (200ms) for the client, 50 packets (1s) for the relay
|
||||
- Minimum: 3 packets (60ms) before playout begins (client), 25 packets (500ms) for relay
|
||||
- Maximum: 250 packets (5s) absolute cap
|
||||
#### MediaHeader (12 bytes)
|
||||
|
||||
The relay uses a deeper buffer because it needs to absorb jitter from the lossy inter-relay link. The client uses a shallower buffer for lower latency since it is on the last hop.
|
||||
```
|
||||
Byte 0: [V:1][T:1][CodecID:4][Q:1][FecRatioHi:1]
|
||||
Byte 1: [FecRatioLo:6][unused:2]
|
||||
Bytes 2-3: sequence (u16 BE)
|
||||
Bytes 4-7: timestamp_ms (u32 BE)
|
||||
Byte 8: fec_block_id (u8)
|
||||
Byte 9: fec_symbol_idx (u8)
|
||||
Byte 10: reserved
|
||||
Byte 11: csrc_count
|
||||
|
||||
**Known issue**: The current jitter buffer does not adapt its depth based on observed jitter. It uses sequence-number ordering only, without timestamp-based playout scheduling. This can lead to drift during long calls, as observed in echo tests.
|
||||
V = version (0), T = is_repair, CodecID = codec, Q = quality_report appended
|
||||
```
|
||||
|
||||
## Browser Audio: AudioWorklet vs ScriptProcessorNode
|
||||
#### MiniHeader (4 bytes, compressed)
|
||||
|
||||
The web bridge (`crates/wzp-web/static/`) uses AudioWorklet as the primary audio I/O mechanism, with ScriptProcessorNode as a fallback.
|
||||
```
|
||||
Bytes 0-1: timestamp_delta_ms (u16 BE)
|
||||
Bytes 2-3: payload_len (u16 BE)
|
||||
|
||||
**AudioWorklet** (preferred):
|
||||
- Runs on a dedicated audio rendering thread
|
||||
- Lower latency (no main-thread round-trip)
|
||||
- Consistent 128-sample callback timing
|
||||
- Supported in Chrome 66+, Firefox 76+, Safari 14.1+
|
||||
Preceded by FRAME_TYPE_MINI (0x01). Full header every 50 frames (~1s).
|
||||
Saves 8 bytes/packet (67% header reduction).
|
||||
```
|
||||
|
||||
**ScriptProcessorNode** (fallback):
|
||||
- Runs on the main thread via `onaudioprocess` callback
|
||||
- Higher latency, potential glitches from main-thread GC pauses
|
||||
- Deprecated by the Web Audio specification
|
||||
- Used when AudioWorklet is not available
|
||||
#### TrunkFrame (batched datagrams)
|
||||
|
||||
Both paths accumulate Float32 samples into 960-sample (20ms) Int16 frames before sending via WebSocket, matching the WZP codec frame size.
|
||||
```
|
||||
[count:u16]
|
||||
[session_id:2][len:u16][payload:len] x count
|
||||
|
||||
**Playback** uses an AudioWorklet with a ring buffer capped at 200ms (9600 samples at 48 kHz). When the buffer exceeds this limit, old samples are dropped to prevent unbounded drift. The fallback path uses scheduled `AudioBufferSourceNode` instances.
|
||||
Packs multiple session packets into one QUIC datagram.
|
||||
Max 10 entries or 1200 bytes, flushed every 5ms.
|
||||
```
|
||||
|
||||
## Room Mode: SFU vs MCU Trade-offs
|
||||
#### QualityReport (4 bytes, optional trailer)
|
||||
|
||||
WarzonePhone implements an **SFU** (Selective Forwarding Unit) architecture:
|
||||
```
|
||||
Byte 0: loss_pct (0-255 maps to 0-100%)
|
||||
Byte 1: rtt_4ms (0-255 maps to 0-1020ms)
|
||||
Byte 2: jitter_ms
|
||||
Byte 3: bitrate_cap_kbps
|
||||
```
|
||||
|
||||
**SFU** (implemented):
|
||||
- Relay forwards each participant's packets to all other participants unchanged
|
||||
- No transcoding -- the relay never decodes or re-encodes audio
|
||||
- O(N) bandwidth at the relay for N participants (each packet is sent N-1 times)
|
||||
- Each client receives separate streams from each other participant
|
||||
- Client must mix/decode multiple streams locally
|
||||
- Lower relay CPU usage (no transcoding)
|
||||
- End-to-end encryption is preserved (relay never sees plaintext)
|
||||
### Bandwidth Summary
|
||||
|
||||
**MCU** (not implemented, for comparison):
|
||||
- Relay would decode all streams, mix them, and re-encode a single combined stream
|
||||
- O(1) bandwidth to each client (receives one mixed stream)
|
||||
- Requires the relay to have codec keys (breaks E2E encryption)
|
||||
- Higher relay CPU (decoding N streams + mixing + re-encoding)
|
||||
- Audio quality loss from re-encoding
|
||||
| Profile | Audio | FEC Overhead | Total | Silence Savings |
|
||||
|---------|-------|-------------|-------|----------------|
|
||||
| Studio 64k | 64 kbps | 10% = 6.4 kbps | **70.4 kbps** | ~50% with DTX |
|
||||
| Studio 48k | 48 kbps | 10% = 4.8 kbps | **52.8 kbps** | ~50% with DTX |
|
||||
| Studio 32k | 32 kbps | 10% = 3.2 kbps | **35.2 kbps** | ~50% with DTX |
|
||||
| Good (24k) | 24 kbps | 20% = 4.8 kbps | **28.8 kbps** | ~50% with DTX |
|
||||
| Degraded (6k) | 6 kbps | 50% = 3.0 kbps | **9.0 kbps** | ~50% with DTX |
|
||||
| Catastrophic (1.2k) | 1.2 kbps | 100% = 1.2 kbps | **2.4 kbps** | ~50% with DTX |
|
||||
|
||||
The SFU choice is driven by the E2E encryption requirement: since relays never have access to the audio codec keys, they cannot decode, mix, or re-encode. The current room implementation in `crates/wzp-relay/src/room.rs` forwards received datagrams to all other participants in the room with best-effort delivery -- if one send fails, the relay continues to the next participant.
|
||||
Additional savings: MiniHeaders save 8 bytes/packet (67% header reduction). Trunking shares QUIC overhead across multiplexed sessions.
|
||||
|
||||
## Security
|
||||
|
||||
### Identity Model
|
||||
|
||||
Every user has a persistent identity derived from a 32-byte seed:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
SEED["32-byte Seed<br/>(BIP39 Mnemonic: 24 words)"] --> HKDF1["HKDF<br/>info='warzone-ed25519'"]
|
||||
SEED --> HKDF2["HKDF<br/>info='warzone-x25519'"]
|
||||
|
||||
HKDF1 --> ED["Ed25519 SigningKey<br/>(Digital Signatures)"]
|
||||
HKDF2 --> X25519["X25519 StaticSecret<br/>(Key Agreement)"]
|
||||
|
||||
ED --> VKEY["Ed25519 VerifyingKey<br/>(Public)"]
|
||||
X25519 --> XPUB["X25519 PublicKey<br/>(Public)"]
|
||||
|
||||
VKEY --> FP["Fingerprint<br/>SHA-256(pubkey), truncated 16 bytes<br/>xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"]
|
||||
|
||||
style SEED fill:#6c5ce7,color:#fff
|
||||
style FP fill:#fd79a8,color:#fff
|
||||
style ED fill:#ee5a24,color:#fff
|
||||
style X25519 fill:#00b894,color:#fff
|
||||
```
|
||||
|
||||
**BIP39 Mnemonic Backup**: The 32-byte seed can be encoded as a 24-word BIP39 mnemonic for human-readable backup. The same seed produces the same identity on any platform.
|
||||
|
||||
**featherChat Compatibility**: The identity derivation is compatible with the Warzone messenger (featherChat), allowing a shared identity across messaging and calling.
|
||||
|
||||
### Cryptographic Handshake
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Caller
|
||||
participant R as Relay / Callee
|
||||
|
||||
Note over C: Derive identity from seed<br/>Ed25519 + X25519 via HKDF
|
||||
|
||||
C->>C: Generate ephemeral X25519 keypair
|
||||
C->>C: Sign(ephemeral_pub || "call-offer")
|
||||
C->>R: CallOffer { identity_pub, ephemeral_pub, signature, profiles }
|
||||
|
||||
R->>R: Verify Ed25519 signature
|
||||
R->>R: Generate ephemeral X25519 keypair
|
||||
R->>R: shared_secret = DH(eph_b, eph_a)
|
||||
R->>R: session_key = HKDF(shared_secret, "warzone-session-key")
|
||||
R->>R: Sign(ephemeral_pub || "call-answer")
|
||||
R->>C: CallAnswer { identity_pub, ephemeral_pub, signature, profile }
|
||||
|
||||
C->>C: Verify signature
|
||||
C->>C: shared_secret = DH(eph_a, eph_b)
|
||||
C->>C: session_key = HKDF(shared_secret)
|
||||
|
||||
Note over C,R: Both have identical ChaCha20-Poly1305 session key
|
||||
C->>R: Encrypted media (QUIC datagrams)
|
||||
R->>C: Encrypted media (QUIC datagrams)
|
||||
|
||||
Note over C,R: Rekey every 65,536 packets<br/>New ephemeral DH + HKDF mix
|
||||
```
|
||||
|
||||
### Encryption Details
|
||||
|
||||
| Component | Algorithm | Purpose |
|
||||
|-----------|-----------|---------|
|
||||
| Identity signing | Ed25519 | Authenticate handshake messages |
|
||||
| Key agreement | X25519 (ephemeral) | Derive shared secret |
|
||||
| Key derivation | HKDF-SHA256 | Derive session key from shared secret |
|
||||
| Media encryption | ChaCha20-Poly1305 | Encrypt audio payloads (16-byte tag) |
|
||||
| Nonce construction | Deterministic from sequence number | No nonce reuse, no state sync needed |
|
||||
| Anti-replay | Sliding window (64-packet) | Reject duplicate/old packets |
|
||||
| Forward secrecy | Rekey every 65,536 packets | New ephemeral DH + HKDF mix |
|
||||
|
||||
**Why ChaCha20-Poly1305 over AES-GCM**:
|
||||
- Faster on hardware without AES-NI (ARM phones, Raspberry Pi relays)
|
||||
- Inherently constant-time (add-rotate-XOR only)
|
||||
- Compatible with Warzone messenger (featherChat)
|
||||
- Same 16-byte authentication tag overhead as AES-GCM
|
||||
|
||||
**AEAD with AAD**: The MediaHeader is used as Associated Authenticated Data. The header is authenticated but not encrypted, allowing relays to read routing information (block ID, sequence number) without decrypting the payload.
|
||||
|
||||
### Trust on First Use (TOFU)
|
||||
|
||||
Clients remember the relay's TLS certificate fingerprint after first connection. If the fingerprint changes on a subsequent connection, the desktop client shows a "Server Key Changed" warning dialog. The relay derives its TLS certificate deterministically from its persisted identity seed, so the fingerprint is stable across restarts.
|
||||
|
||||
## Relay Architecture
|
||||
|
||||
### Room Mode (Default SFU)
|
||||
|
||||
In room mode, the relay acts as a Selective Forwarding Unit. Clients join named rooms via the QUIC SNI (Server Name Indication) field. The relay forwards each participant's encrypted packets to all other participants in the room without decoding or re-encoding.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Room Mode (SFU)"
|
||||
C1[Client 1] -->|"QUIC SNI=room-hash"| RM[Room Manager]
|
||||
C2[Client 2] -->|"QUIC SNI=room-hash"| RM
|
||||
C3[Client 3] -->|"QUIC SNI=room-hash"| RM
|
||||
RM --> R1[Room 'podcast']
|
||||
R1 -->|fan-out| C1
|
||||
R1 -->|fan-out| C2
|
||||
R1 -->|fan-out| C3
|
||||
end
|
||||
|
||||
style RM fill:#ff9f43,color:#fff
|
||||
style R1 fill:#fdcb6e
|
||||
```
|
||||
|
||||
**SFU vs MCU trade-off**: SFU was chosen because it preserves end-to-end encryption (the relay never sees plaintext audio). An MCU would need to decode, mix, and re-encode, breaking E2E encryption. The trade-off is O(N) bandwidth at the relay for N participants.
|
||||
|
||||
### Forward Mode
|
||||
|
||||
With `--remote`, the relay forwards all traffic to a remote relay. Used for chaining relays across lossy or censored links:
|
||||
|
||||
```
|
||||
Client --> Relay A (--remote B) --> Relay B --> Destination Client
|
||||
```
|
||||
|
||||
The relay pipeline in forward mode: FEC decode, jitter buffer, then FEC re-encode for the next hop.
|
||||
|
||||
## Federation
|
||||
|
||||
### Overview
|
||||
|
||||
Two or more relays form a federation mesh. Each relay is an independent SFU. When configured to trust each other, they bridge **global rooms** -- participants on relay A in a global room hear participants on relay B in the same room.
|
||||
|
||||
### Configuration
|
||||
|
||||
Federation uses three TOML configuration sections:
|
||||
|
||||
- `[[peers]]` -- outbound connections to peer relays (url + TLS fingerprint)
|
||||
- `[[trusted]]` -- inbound connections accepted from relays (TLS fingerprint only)
|
||||
- `[[global_rooms]]` -- room names to bridge across all federated peers
|
||||
|
||||
### Federation Topology
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Relay A (EU)"
|
||||
A_RM[Room Manager]
|
||||
A_FM[Federation Manager]
|
||||
A1[Alice - local]
|
||||
A2[Bob - local]
|
||||
A_RM --> A_FM
|
||||
end
|
||||
|
||||
subgraph "Relay B (US)"
|
||||
B_RM[Room Manager]
|
||||
B_FM[Federation Manager]
|
||||
B1[Charlie - local]
|
||||
B_RM --> B_FM
|
||||
end
|
||||
|
||||
A_FM <-->|"QUIC SNI='_federation'<br/>GlobalRoomActive/Inactive<br/>Media forwarding"| B_FM
|
||||
|
||||
A1 -->|media| A_RM
|
||||
A2 -->|media| A_RM
|
||||
B1 -->|media| B_RM
|
||||
|
||||
A_RM -->|"federated fan-out"| A1
|
||||
A_RM -->|"federated fan-out"| A2
|
||||
B_RM -->|"federated fan-out"| B1
|
||||
|
||||
style A_FM fill:#6c5ce7,color:#fff
|
||||
style B_FM fill:#6c5ce7,color:#fff
|
||||
style A_RM fill:#ff9f43,color:#fff
|
||||
style B_RM fill:#ff9f43,color:#fff
|
||||
```
|
||||
|
||||
### Protocol
|
||||
|
||||
1. On startup, each relay connects to all configured `[[peers]]` via QUIC with SNI `"_federation"`
|
||||
2. After QUIC handshake, sends `FederationHello { tls_fingerprint }` for identity verification
|
||||
3. Peer verifies the fingerprint against its `[[trusted]]` or `[[peers]]` list
|
||||
4. When a local participant joins a global room, sends `GlobalRoomActive { room }` to all peers
|
||||
5. When the last local participant leaves, sends `GlobalRoomInactive { room }`
|
||||
6. Media is forwarded as `[room_hash:8][original_media_packet]` -- the relay does not decrypt
|
||||
|
||||
### What Relays Do NOT Do
|
||||
|
||||
- **No transcoding** -- media passes through as-is
|
||||
- **No re-encryption** -- packets are already encrypted E2E
|
||||
- **No central coordinator** -- each relay independently connects to configured peers
|
||||
- **No automatic peer discovery** -- peers must be explicitly configured
|
||||
|
||||
### Failure Handling
|
||||
|
||||
- If a peer goes down, local rooms continue working; federated participants disappear from presence
|
||||
- Reconnection: every 30 seconds with exponential backoff up to 5 minutes
|
||||
- If a peer restarts with a different identity, the fingerprint check fails with a clear log message
|
||||
|
||||
## Jitter Buffer
|
||||
|
||||
The jitter buffer balances latency vs quality:
|
||||
|
||||
| Setting | Client | Relay |
|
||||
|---------|--------|-------|
|
||||
| Target depth | 10 packets (200ms) | 50 packets (1s) |
|
||||
| Minimum before playout | 3 packets (60ms) | 25 packets (500ms) |
|
||||
| Maximum cap | 250 packets (5s) | 250 packets (5s) |
|
||||
|
||||
The relay uses a deeper buffer to absorb jitter from lossy inter-relay links. The client uses a shallower buffer for lower latency.
|
||||
|
||||
The adaptive playout delay tracks jitter via exponential moving average and adjusts the target depth:
|
||||
|
||||
```
|
||||
target_delay = ceil(jitter_ema / 20ms) + 2
|
||||
```
|
||||
|
||||
**Known limitation**: The current jitter buffer does not use timestamp-based playout scheduling. It relies on sequence-number ordering only, which can lead to drift during long calls.
|
||||
|
||||
## Signal Messages
|
||||
|
||||
Signal messages are sent over reliable QUIC streams as length-prefixed JSON:
|
||||
|
||||
```
|
||||
[4-byte length prefix][serde_json payload]
|
||||
```
|
||||
|
||||
| Message | Purpose |
|
||||
|---------|---------|
|
||||
| `CallOffer` | Identity, ephemeral key, signature, supported profiles |
|
||||
| `CallAnswer` | Identity, ephemeral key, signature, chosen profile |
|
||||
| `AuthToken` | featherChat bearer token for relay authentication |
|
||||
| `Hangup` | Reason: Normal, Busy, Declined, Timeout, Error |
|
||||
| `Hold` / `Unhold` | Call hold state |
|
||||
| `Mute` / `Unmute` | Mic mute state |
|
||||
| `Transfer` | Call transfer to another relay/fingerprint |
|
||||
| `Rekey` | New ephemeral key for forward secrecy |
|
||||
| `QualityUpdate` | Quality report + recommended profile |
|
||||
| `Ping` / `Pong` | Latency measurement (timestamp_ms) |
|
||||
| `RoomUpdate` | Participant list changes |
|
||||
| `PresenceUpdate` | Federation presence gossip |
|
||||
| `RouteQuery` / `RouteResponse` | Presence discovery for routing |
|
||||
| `FederationHello` | Relay identity during federation setup |
|
||||
| `GlobalRoomActive` / `GlobalRoomInactive` | Federation room bridging |
|
||||
|
||||
## Test Coverage
|
||||
|
||||
272 tests across all crates, 0 failures:
|
||||
|
||||
| Crate | Tests | Key Coverage |
|
||||
|-------|-------|-------------|
|
||||
| wzp-proto | 41 | Wire format, jitter buffer, quality tiers, mini-frames, trunking |
|
||||
| wzp-codec | 31 | Opus/Codec2 roundtrip, silence detection, noise suppression |
|
||||
| wzp-fec | 22 | RaptorQ encode/decode, loss recovery, interleaving |
|
||||
| wzp-crypto | 34 + 28 compat | Encrypt/decrypt, handshake, anti-replay, featherChat identity |
|
||||
| wzp-transport | 2 | QUIC connection setup |
|
||||
| wzp-relay | 40 + 4 integration | Room ACL, session mgmt, metrics, probes, mesh, trunking |
|
||||
| wzp-client | 30 + 2 integration | Encoder/decoder, quality adapter, silence, drift, sweep |
|
||||
| wzp-web | 2 | Metrics |
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- **Rust** 1.85+ (2024 edition)
|
||||
- **Linux**: cmake, pkg-config, libasound2-dev (for audio feature)
|
||||
- **macOS**: Xcode command line tools (CoreAudio included)
|
||||
- **Android**: NDK r27c, cmake 3.28+ (from pip)
|
||||
|
||||
459
docs/USER_GUIDE.md
Normal file
459
docs/USER_GUIDE.md
Normal file
@@ -0,0 +1,459 @@
|
||||
# WarzonePhone User Guide
|
||||
|
||||
This guide covers all WarzonePhone client applications: Desktop (Tauri), Android, CLI, and Web.
|
||||
|
||||
## Desktop Client (Tauri)
|
||||
|
||||
The desktop client is a Tauri application with a native Rust audio engine and a web-based UI. It runs on macOS, Windows, and Linux.
|
||||
|
||||
### Connect Screen
|
||||
|
||||
When you launch the desktop client, you see the connect screen with:
|
||||
|
||||
- **Relay selector** -- click the relay button to open the Manage Relays dialog. Shows relay name, address, connection status (verified/new/changed/offline), and RTT latency
|
||||
- **Room** -- enter a room name. Clients in the same room hear each other. Room names are hashed before being sent to the relay for privacy
|
||||
- **Alias** -- your display name shown to other participants
|
||||
- **OS Echo Cancel** -- checkbox to enable macOS VoiceProcessingIO (Apple's FaceTime-grade AEC). Strongly recommended when using speakers
|
||||
- **Connect button** -- connects to the selected relay and joins the room
|
||||
- **Identity info** -- your identicon and fingerprint are shown at the bottom. Click to copy
|
||||
|
||||
Recent rooms are displayed below the form for quick reconnection. Click any recent room to select it and its associated relay.
|
||||
|
||||
### In-Call Screen
|
||||
|
||||
Once connected, the in-call screen shows:
|
||||
|
||||
- **Room name** and **call timer** at the top
|
||||
- **Status indicator** -- green when connected, yellow when reconnecting
|
||||
- **Audio level meter** -- real-time visualization of outgoing audio
|
||||
- **Participant list** -- identicon, alias, and fingerprint for each participant. Your own entry is highlighted with a badge
|
||||
- **Controls** -- Mic toggle, Hang Up, Speaker toggle
|
||||
- **Stats bar** -- TX and RX frame rates
|
||||
|
||||
### Settings Panel
|
||||
|
||||
Open with the gear icon or **Cmd+,** (Ctrl+, on Windows/Linux). Contains:
|
||||
|
||||
#### Connection
|
||||
|
||||
- **Default Room** -- room name used on next connect
|
||||
- **Alias** -- display name
|
||||
|
||||
#### Audio
|
||||
|
||||
- **Quality slider** -- 5 levels:
|
||||
|
||||
| Position | Profile | Description |
|
||||
|----------|---------|-------------|
|
||||
| 0 | Auto | Adaptive quality based on network conditions |
|
||||
| 1 | Opus 24k | Good conditions (28.8 kbps with FEC) |
|
||||
| 2 | Opus 6k | Degraded conditions (9.0 kbps with FEC) |
|
||||
| 3 | Codec2 3.2k | Poor conditions (4.8 kbps with FEC) |
|
||||
| 4 | Codec2 1.2k | Catastrophic conditions (2.4 kbps with FEC) |
|
||||
|
||||
- **OS Echo Cancellation** -- macOS VoiceProcessingIO toggle
|
||||
- **Automatic Gain Control** -- normalize mic volume
|
||||
|
||||
#### Identity
|
||||
|
||||
- **Fingerprint** -- your public identity fingerprint
|
||||
- **Identity file** -- stored at `~/.wzp/identity`
|
||||
|
||||
#### Recent Rooms
|
||||
|
||||
- History of recently joined rooms with relay association
|
||||
- Clear History button
|
||||
|
||||
### Manage Relays Dialog
|
||||
|
||||
Open by clicking the relay selector button on the connect screen:
|
||||
|
||||
- **Relay list** -- each entry shows name, address, identicon (from server fingerprint), lock status, and RTT
|
||||
- **Select** -- click a relay to make it the default
|
||||
- **Remove** -- click the X button to delete a relay
|
||||
- **Add Relay** -- enter name and host:port to add a new relay
|
||||
- **Ping** -- relays are automatically pinged when the dialog opens. RTT and server fingerprint are updated
|
||||
|
||||
### Key Change Warning Dialog
|
||||
|
||||
If a relay's TLS fingerprint has changed since your last connection, a warning dialog appears:
|
||||
|
||||
- Shows the previously known fingerprint and the new fingerprint
|
||||
- **Accept New Key** -- trust the new fingerprint and proceed
|
||||
- **Cancel** -- abort the connection
|
||||
|
||||
This is the TOFU (Trust on First Use) model. Fingerprint changes typically mean the relay was restarted with a new identity. However, they could also indicate a man-in-the-middle attack.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action | Context |
|
||||
|----------|--------|---------|
|
||||
| **m** | Toggle microphone | In-call |
|
||||
| **s** | Toggle speaker | In-call |
|
||||
| **q** | Hang up | In-call |
|
||||
| **Cmd+,** (Ctrl+,) | Open/close settings | Any |
|
||||
| **Escape** | Close dialog/settings | Any |
|
||||
| **Enter** | Connect | Connect screen (when room/alias field is focused) |
|
||||
|
||||
### Audio Engine
|
||||
|
||||
The desktop audio engine uses:
|
||||
|
||||
- **CPAL** for audio I/O (CoreAudio on macOS, WASAPI on Windows, ALSA on Linux)
|
||||
- **VoiceProcessingIO** on macOS for OS-level echo cancellation (opt-in via checkbox)
|
||||
- **Lock-free SPSC ring buffers** between audio threads and network threads
|
||||
- **Direct playout** -- no jitter buffer on the client (the relay buffers instead)
|
||||
- Audio callbacks deliver 512 f32 samples at 48 kHz on macOS (accumulated to 960-sample frames for codec)
|
||||
|
||||
#### Audio Quality Notes
|
||||
|
||||
- Always use **Release builds** for real-time audio. Debug builds are too slow for wzp-codec, nnnoiseless, audiopus, and raptorq
|
||||
- VoiceProcessingIO is strongly recommended on macOS. Software AEC does not work well with the round-trip latency (~35-45ms)
|
||||
- The quality slider only affects the **encode** side. Decoding always accepts all codecs
|
||||
|
||||
### Auto-Reconnect
|
||||
|
||||
If the connection drops, the client automatically attempts to reconnect with exponential backoff (1s, 2s, 4s, 8s, capped at 10s). After 5 failed attempts, the client returns to the connect screen. The status dot shows yellow during reconnection.
|
||||
|
||||
## Android Client
|
||||
|
||||
The Android client is built with Kotlin and Jetpack Compose, using JNI to call the Rust audio engine.
|
||||
|
||||
### Call Screen
|
||||
|
||||
The main call screen shows:
|
||||
|
||||
- **Server selector** -- tap to choose from configured servers
|
||||
- **Room name** -- enter the room to join
|
||||
- **Connect/Disconnect** button
|
||||
- **Participant list** with identicons and aliases
|
||||
- **Audio level visualization**
|
||||
- **Mute/Unmute** button
|
||||
|
||||
### Settings Screen
|
||||
|
||||
The settings screen is organized into sections:
|
||||
|
||||
#### Identity
|
||||
|
||||
- **Display Name** -- your alias shown to other participants
|
||||
- **Fingerprint** -- displayed with an identicon. Tap to copy
|
||||
- **Copy Key** -- copy the 64-character hex seed to clipboard for backup
|
||||
- **Restore Key** -- paste a previously backed-up hex seed to restore your identity
|
||||
|
||||
#### Audio Defaults
|
||||
|
||||
- **Voice Volume** -- playout gain slider (-20 dB to +20 dB)
|
||||
- **Mic Gain** -- capture gain slider (-20 dB to +20 dB)
|
||||
- **Echo Cancellation (AEC)** -- toggle Android's built-in AEC. Disable if audio sounds distorted
|
||||
- **Quality slider** -- 8 levels from best to lowest:
|
||||
|
||||
| Position | Profile | Bitrate | Color |
|
||||
|----------|---------|---------|-------|
|
||||
| 0 | Studio 64k | 70.4 kbps | Green |
|
||||
| 1 | Studio 48k | 52.8 kbps | Green |
|
||||
| 2 | Studio 32k | 35.2 kbps | Green |
|
||||
| 3 | Auto | Adaptive | Yellow-green |
|
||||
| 4 | Opus 24k | 28.8 kbps | Yellow-green |
|
||||
| 5 | Opus 6k | 9.0 kbps | Yellow |
|
||||
| 6 | Codec2 3.2k | 4.8 kbps | Orange |
|
||||
| 7 | Codec2 1.2k | 2.4 kbps | Red |
|
||||
|
||||
Note: "Decode always accepts all codecs" -- the quality setting only affects encoding.
|
||||
|
||||
#### Servers
|
||||
|
||||
- **Server chips** -- tap to select, X to remove (built-in servers cannot be removed)
|
||||
- **Add Server** -- enter host, port (default 4433), and optional label
|
||||
- **Force Ping** -- servers are pinged on dialog open to measure RTT
|
||||
|
||||
#### Network
|
||||
|
||||
- **Prefer IPv6** -- toggle to prefer IPv6 connections when available
|
||||
|
||||
#### Room
|
||||
|
||||
- **Default Room** -- the room name pre-filled on the call screen
|
||||
|
||||
### Identity Backup and Restore
|
||||
|
||||
Your identity is a 32-byte seed stored as a 64-character hex string. To back up:
|
||||
|
||||
1. Go to Settings > Identity
|
||||
2. Tap **Copy Key**
|
||||
3. Store the hex string securely
|
||||
|
||||
To restore on a new device:
|
||||
|
||||
1. Go to Settings > Identity
|
||||
2. Tap **Restore Key**
|
||||
3. Paste the 64-character hex string
|
||||
4. Tap **Restore** (key is staged)
|
||||
5. Tap **Save** to apply
|
||||
|
||||
The same seed produces the same fingerprint on any device or platform.
|
||||
|
||||
## CLI Client (wzp-client)
|
||||
|
||||
The CLI client is a command-line tool for testing, recording, and live audio.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
wzp-client [options] [relay-addr]
|
||||
```
|
||||
|
||||
Default relay address: `127.0.0.1:4433`
|
||||
|
||||
### Flags Reference
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--live` | Live mic/speaker mode. Requires `--features audio` at build time |
|
||||
| `--send-tone <secs>` | Send a 440 Hz test tone for N seconds |
|
||||
| `--send-file <file>` | Send a raw PCM file (48 kHz mono s16le) |
|
||||
| `--record <file.raw>` | Record received audio to raw PCM file |
|
||||
| `--echo-test <secs>` | Run automated echo quality test for N seconds. Produces a windowed analysis with loss%, SNR, correlation |
|
||||
| `--drift-test <secs>` | Run automated clock-drift measurement for N seconds |
|
||||
| `--sweep` | Run jitter buffer parameter sweep (local, no network). Tests different buffer configurations |
|
||||
| `--seed <hex>` | Identity seed as 64 hex characters. Compatible with featherChat |
|
||||
| `--mnemonic <words...>` | Identity seed as BIP39 mnemonic (24 words). All remaining non-flag words are consumed |
|
||||
| `--room <name>` | Room name. Hashed before sending for privacy |
|
||||
| `--token <token>` | featherChat bearer token for relay authentication |
|
||||
| `--metrics-file <path>` | Write JSONL telemetry to file (1 line/sec) |
|
||||
| `--help`, `-h` | Print help and exit |
|
||||
|
||||
### Common Usage Patterns
|
||||
|
||||
#### Connectivity Test (Silence)
|
||||
|
||||
```bash
|
||||
# Send 250 silence frames (5 seconds) and exit
|
||||
wzp-client 127.0.0.1:4433
|
||||
```
|
||||
|
||||
#### Live Audio Call
|
||||
|
||||
```bash
|
||||
# Terminal 1
|
||||
wzp-relay
|
||||
|
||||
# Terminal 2: Alice
|
||||
wzp-client --live --room myroom 127.0.0.1:4433
|
||||
|
||||
# Terminal 3: Bob
|
||||
wzp-client --live --room myroom 127.0.0.1:4433
|
||||
```
|
||||
|
||||
Both capture from mic and play received audio. Press Ctrl+C to stop.
|
||||
|
||||
#### Send Test Tone and Record
|
||||
|
||||
```bash
|
||||
# Terminal 1
|
||||
wzp-relay
|
||||
|
||||
# Terminal 2: Send 10 seconds of 440 Hz tone
|
||||
wzp-client --send-tone 10 127.0.0.1:4433
|
||||
|
||||
# Terminal 3: Record what is received
|
||||
wzp-client --record call.raw 127.0.0.1:4433
|
||||
```
|
||||
|
||||
Play the recording:
|
||||
|
||||
```bash
|
||||
ffplay -f s16le -ar 48000 -ac 1 call.raw
|
||||
```
|
||||
|
||||
#### Send Audio File
|
||||
|
||||
```bash
|
||||
# Convert to raw PCM first
|
||||
ffmpeg -i song.mp3 -f s16le -ar 48000 -ac 1 song.raw
|
||||
|
||||
# Send through relay
|
||||
wzp-client --send-file song.raw 127.0.0.1:4433
|
||||
```
|
||||
|
||||
#### Echo Quality Test
|
||||
|
||||
```bash
|
||||
wzp-relay &
|
||||
wzp-client --echo-test 30 127.0.0.1:4433
|
||||
```
|
||||
|
||||
Produces a windowed analysis showing loss percentage, SNR, correlation, and quality degradation trends.
|
||||
|
||||
#### Clock Drift Test
|
||||
|
||||
```bash
|
||||
wzp-relay &
|
||||
wzp-client --drift-test 60 127.0.0.1:4433
|
||||
```
|
||||
|
||||
Measures clock drift between the send and receive paths over the specified duration.
|
||||
|
||||
#### Jitter Buffer Sweep
|
||||
|
||||
```bash
|
||||
# Runs locally, no network needed
|
||||
wzp-client --sweep
|
||||
```
|
||||
|
||||
Tests different jitter buffer configurations and prints results.
|
||||
|
||||
#### With Identity and Auth
|
||||
|
||||
```bash
|
||||
# Using hex seed
|
||||
wzp-client --seed 0123456789abcdef...64chars --room secure-room --token my-bearer-token relay.example.com:4433
|
||||
|
||||
# Using BIP39 mnemonic
|
||||
wzp-client --mnemonic abandon abandon abandon ... zoo --room secure-room relay.example.com:4433
|
||||
```
|
||||
|
||||
#### With JSONL Telemetry
|
||||
|
||||
```bash
|
||||
wzp-client --live --metrics-file /tmp/call.jsonl relay.example.com:4433
|
||||
```
|
||||
|
||||
Writes one JSON object per second:
|
||||
|
||||
```json
|
||||
{
|
||||
"ts": "2026-04-07T12:00:00Z",
|
||||
"buffer_depth": 45,
|
||||
"underruns": 0,
|
||||
"overruns": 0,
|
||||
"loss_pct": 1.2,
|
||||
"rtt_ms": 34,
|
||||
"jitter_ms": 8,
|
||||
"frames_sent": 50,
|
||||
"frames_received": 49,
|
||||
"quality_profile": "GOOD"
|
||||
}
|
||||
```
|
||||
|
||||
### Audio File Format
|
||||
|
||||
All raw PCM files use:
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Sample rate | 48 kHz |
|
||||
| Channels | 1 (mono) |
|
||||
| Sample format | signed 16-bit little-endian (s16le) |
|
||||
|
||||
Conversion commands:
|
||||
|
||||
```bash
|
||||
# WAV to raw PCM
|
||||
ffmpeg -i input.wav -f s16le -ar 48000 -ac 1 output.raw
|
||||
|
||||
# MP3 to raw PCM
|
||||
ffmpeg -i input.mp3 -f s16le -ar 48000 -ac 1 output.raw
|
||||
|
||||
# Raw PCM to WAV
|
||||
ffmpeg -f s16le -ar 48000 -ac 1 -i input.raw output.wav
|
||||
|
||||
# Play raw PCM
|
||||
ffplay -f s16le -ar 48000 -ac 1 file.raw
|
||||
```
|
||||
|
||||
## Web Client (Browser)
|
||||
|
||||
The web client runs in a browser via the wzp-web bridge server.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Start relay
|
||||
wzp-relay
|
||||
|
||||
# Start web bridge
|
||||
wzp-web --port 8080 --relay 127.0.0.1:4433
|
||||
|
||||
# For remote access (requires TLS for mic)
|
||||
wzp-web --port 8443 --relay 127.0.0.1:4433 --tls
|
||||
```
|
||||
|
||||
Open `http://localhost:8080/room-name` (or `https://...` with TLS).
|
||||
|
||||
### Features
|
||||
|
||||
- **Open mic** (default) and **push-to-talk** modes
|
||||
- PTT via on-screen button, mouse hold, or spacebar
|
||||
- Audio level meter
|
||||
- Auto-reconnection on disconnect
|
||||
|
||||
### Audio Processing
|
||||
|
||||
The web client uses AudioWorklet (preferred) with a ScriptProcessorNode fallback:
|
||||
|
||||
- **Capture**: Accumulates Float32 samples into 960-sample (20ms) Int16 frames
|
||||
- **Playback**: Ring buffer capped at 200ms (9600 samples at 48 kHz)
|
||||
|
||||
## Identity System
|
||||
|
||||
### Overview
|
||||
|
||||
Your identity is a 32-byte cryptographic seed that derives:
|
||||
|
||||
- **Ed25519 signing key** -- authenticates handshake messages
|
||||
- **X25519 key agreement key** -- derives shared session encryption keys
|
||||
- **Fingerprint** -- SHA-256 of the public key, truncated to 16 bytes, displayed as `xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx`
|
||||
- **Identicon** -- deterministic visual avatar generated from the fingerprint
|
||||
|
||||
### Seed Sources
|
||||
|
||||
| Source | Description |
|
||||
|--------|-------------|
|
||||
| Auto-generated | Created on first run, stored in `~/.wzp/identity` (desktop/CLI) or app storage (Android) |
|
||||
| `--seed <hex>` | 64-character hex string (CLI) |
|
||||
| `--mnemonic <words>` | 24-word BIP39 mnemonic (CLI) |
|
||||
| Copy Key / Restore Key | Hex backup/restore (Android settings) |
|
||||
|
||||
### BIP39 Mnemonic Backup
|
||||
|
||||
The 32-byte seed can be represented as a 24-word BIP39 mnemonic for human-readable backup. The same mnemonic produces the same identity on any platform or device.
|
||||
|
||||
### featherChat Compatibility
|
||||
|
||||
The identity derivation uses the same HKDF scheme as featherChat (Warzone messenger). The same seed produces the same fingerprint in both systems, allowing a unified identity across messaging and calling.
|
||||
|
||||
### Trust on First Use (TOFU)
|
||||
|
||||
Clients remember the fingerprints of relays and peers they connect to. On subsequent connections, if a fingerprint changes, the client warns the user. This protects against man-in-the-middle attacks but requires manual verification on first contact.
|
||||
|
||||
## Quality Profiles Explained
|
||||
|
||||
### When to Use Each Profile
|
||||
|
||||
| Profile | Total Bandwidth | Best For | Trade-offs |
|
||||
|---------|----------------|----------|------------|
|
||||
| **Studio 64k** | 70.4 kbps | LAN calls, music, podcasting | Highest quality, needs good network |
|
||||
| **Studio 48k** | 52.8 kbps | Good WiFi, wired connections | Near-studio quality |
|
||||
| **Studio 32k** | 35.2 kbps | Reliable WiFi, LTE | Very good quality with lower bandwidth |
|
||||
| **Auto** | Adaptive | Most users | Automatically switches based on network conditions |
|
||||
| **Opus 24k** | 28.8 kbps | General use, moderate networks | Good speech quality, reasonable bandwidth |
|
||||
| **Opus 6k** | 9.0 kbps | 3G networks, congested WiFi | Intelligible speech, some artifacts |
|
||||
| **Codec2 3.2k** | 4.8 kbps | Poor connections | Robotic but intelligible, narrowband |
|
||||
| **Codec2 1.2k** | 2.4 kbps | Satellite links, extreme loss | Minimal intelligibility, last resort |
|
||||
|
||||
### Auto Mode
|
||||
|
||||
Auto mode starts at the **Good (Opus 24k)** profile and adapts based on observed network quality:
|
||||
|
||||
- **Downgrade** -- 3 consecutive bad quality reports (2 on cellular) trigger a step down
|
||||
- **Upgrade** -- 10 consecutive good quality reports trigger a step up (one tier at a time)
|
||||
- **Network handoff** -- switching from WiFi to cellular triggers a preemptive one-tier downgrade plus a 10-second FEC boost
|
||||
|
||||
Auto mode uses three tiers (Good, Degraded, Catastrophic). It does not use the Studio profiles, which must be selected manually.
|
||||
|
||||
### Manual Override
|
||||
|
||||
When you select a specific profile (not Auto), adaptive switching is disabled. The encoder stays at the selected profile regardless of network conditions. This is useful when you know your network quality and want consistent encoding, or when you want to force a specific bitrate.
|
||||
|
||||
Note: The decoder always accepts all codecs. A manual quality selection only affects what you send, not what you receive.
|
||||
Reference in New Issue
Block a user