docs: protocol audit 2026-05-25, update architecture + Obsidian vault

Audit:
- docs/AUDIT-2026-05-25.md: full protocol audit covering 8 findings
  (4 critical, 2 high, 5 medium, 4 low) with code references and fix
  effort estimates
- vault/Audit/Tasks.md: Obsidian Tasks plugin file tracking all audit
  items with priorities, due dates, and per-step checklists

Architecture docs updated for Wire format v2 and Wave 5/6 features:
- ARCHITECTURE.md: adds wzp-video to dependency graph and project
  structure; wire format updated to v2 (16B header, 5B MiniHeader);
  relay concurrency section corrected (DashMap+RwLock is current, not
  a future optimization); test count 571→702; Android note
- PROGRESS.md: Wave 5 and Wave 6 sections appended; test count 372→702;
  current status and open blockers as of 2026-05-25
- ROAD-TO-VIDEO.md: implementation status table inserted (/🟡/🔴/🔲
  per phase); 6-step critical path to first video call
- WZP-SPEC.md: MediaHeader updated to v2 (16B byte-aligned); MiniHeader
  updated to 5B with seq_delta; codec IDs 9-12 added (H.264/H.265/AV1);
  version negotiation section added

Obsidian vault (vault/):
- 114 files across Architecture/, PRDs/, Reports/, Android/,
  Reference/, Audit/ with YAML frontmatter
- 00 - Home.md index note with wiki links
- .obsidian/app.json config

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-05-25 06:00:17 +04:00
parent 12b0d9738f
commit ed8a7ae5aa
120 changed files with 22781 additions and 65 deletions

682
vault/Reference/API.md Normal file
View File

@@ -0,0 +1,682 @@
---
tags: [reference, wzp]
type: reference
---
# WarzonePhone Crate API Reference
## wzp-proto
**Path**: `crates/wzp-proto/src/`
The protocol definition crate. Contains all shared types, trait interfaces, and core logic. No implementation dependencies -- this is the hub of the star dependency graph.
### Traits (`traits.rs`)
```rust
/// Encodes PCM audio into compressed frames.
pub trait AudioEncoder: Send + Sync {
fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result<usize, CodecError>;
fn codec_id(&self) -> CodecId;
fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError>;
fn max_frame_bytes(&self) -> usize;
fn set_inband_fec(&mut self, _enabled: bool) {} // default no-op
fn set_dtx(&mut self, _enabled: bool) {} // default no-op
}
/// Decodes compressed frames back to PCM audio.
pub trait AudioDecoder: Send + Sync {
fn decode(&mut self, encoded: &[u8], pcm: &mut [i16]) -> Result<usize, CodecError>;
fn decode_lost(&mut self, pcm: &mut [i16]) -> Result<usize, CodecError>;
fn codec_id(&self) -> CodecId;
fn set_profile(&mut self, profile: QualityProfile) -> Result<(), CodecError>;
}
/// Encodes source symbols into FEC-protected blocks.
pub trait FecEncoder: Send + Sync {
fn add_source_symbol(&mut self, data: &[u8]) -> Result<(), FecError>;
fn generate_repair(&mut self, ratio: f32) -> Result<Vec<(u8, Vec<u8>)>, FecError>;
fn finalize_block(&mut self) -> Result<u8, FecError>;
fn current_block_id(&self) -> u8;
fn current_block_size(&self) -> usize;
}
/// Decodes FEC-protected blocks, recovering lost source symbols.
pub trait FecDecoder: Send + Sync {
fn add_symbol(&mut self, block_id: u8, symbol_index: u8, is_repair: bool, data: &[u8]) -> Result<(), FecError>;
fn try_decode(&mut self, block_id: u8) -> Result<Option<Vec<Vec<u8>>>, FecError>;
fn expire_before(&mut self, block_id: u8);
}
/// Per-call encryption session (symmetric, after key exchange).
pub trait CryptoSession: Send + Sync {
fn encrypt(&mut self, header_bytes: &[u8], plaintext: &[u8], out: &mut Vec<u8>) -> Result<(), CryptoError>;
fn decrypt(&mut self, header_bytes: &[u8], ciphertext: &[u8], out: &mut Vec<u8>) -> Result<(), CryptoError>;
fn initiate_rekey(&mut self) -> Result<[u8; 32], CryptoError>;
fn complete_rekey(&mut self, peer_ephemeral_pub: &[u8; 32]) -> Result<(), CryptoError>;
fn overhead(&self) -> usize { 16 } // ChaCha20-Poly1305 tag
}
/// Key exchange using the Warzone identity model.
pub trait KeyExchange: Send + Sync {
fn from_identity_seed(seed: &[u8; 32]) -> Self where Self: Sized;
fn generate_ephemeral(&mut self) -> [u8; 32];
fn identity_public_key(&self) -> [u8; 32];
fn fingerprint(&self) -> [u8; 16];
fn sign(&self, data: &[u8]) -> Vec<u8>;
fn verify(peer_identity_pub: &[u8; 32], data: &[u8], signature: &[u8]) -> bool where Self: Sized;
fn derive_session(&self, peer_ephemeral_pub: &[u8; 32]) -> Result<Box<dyn CryptoSession>, CryptoError>;
}
/// Transport layer for sending/receiving media and signaling.
#[async_trait]
pub trait MediaTransport: Send + Sync {
async fn send_media(&self, packet: &MediaPacket) -> Result<(), TransportError>;
async fn recv_media(&self) -> Result<Option<MediaPacket>, TransportError>;
async fn send_signal(&self, msg: &SignalMessage) -> Result<(), TransportError>;
async fn recv_signal(&self) -> Result<Option<SignalMessage>, TransportError>;
fn path_quality(&self) -> PathQuality;
async fn close(&self) -> Result<(), TransportError>;
}
/// Wraps/unwraps packets for DPI evasion (Phase 2).
pub trait ObfuscationLayer: Send + Sync {
fn obfuscate(&mut self, data: &[u8], out: &mut Vec<u8>) -> Result<(), ObfuscationError>;
fn deobfuscate(&mut self, data: &[u8], out: &mut Vec<u8>) -> Result<(), ObfuscationError>;
}
/// Adaptive quality controller.
pub trait QualityController: Send + Sync {
fn observe(&mut self, report: &QualityReport) -> Option<QualityProfile>;
fn force_profile(&mut self, profile: QualityProfile);
fn current_profile(&self) -> QualityProfile;
}
```
### Wire Format Types (`packet.rs`)
```rust
pub struct MediaHeader { /* 12 bytes */ }
pub struct QualityReport { /* 4 bytes */ }
pub struct MediaPacket { pub header: MediaHeader, pub payload: Bytes, pub quality_report: Option<QualityReport> }
pub enum SignalMessage { CallOffer{..}, CallAnswer{..}, IceCandidate{..}, Rekey{..}, QualityUpdate{..}, Ping{..}, Pong{..}, Hangup{..} }
pub enum HangupReason { Normal, Busy, Declined, Timeout, Error }
```
Key methods:
- `MediaHeader::write_to(&self, buf: &mut impl BufMut)` -- serialize to 12 bytes
- `MediaHeader::read_from(buf: &mut impl Buf) -> Option<Self>` -- deserialize
- `MediaHeader::encode_fec_ratio(ratio: f32) -> u8` -- float to 7-bit wire encoding
- `MediaHeader::decode_fec_ratio(encoded: u8) -> f32` -- 7-bit wire to float
- `MediaPacket::to_bytes(&self) -> Bytes` -- serialize complete packet
- `MediaPacket::from_bytes(data: Bytes) -> Option<Self>` -- deserialize
### Codec Identifiers (`codec_id.rs`)
```rust
pub enum CodecId { Opus24k = 0, Opus16k = 1, Opus6k = 2, Codec2_3200 = 3, Codec2_1200 = 4 }
pub struct QualityProfile {
pub codec: CodecId,
pub fec_ratio: f32,
pub frame_duration_ms: u8,
pub frames_per_block: u8,
}
```
Constants: `QualityProfile::GOOD`, `QualityProfile::DEGRADED`, `QualityProfile::CATASTROPHIC`
Key methods:
- `CodecId::bitrate_bps(self) -> u32`
- `CodecId::frame_duration_ms(self) -> u8`
- `CodecId::sample_rate_hz(self) -> u32`
- `CodecId::from_wire(val: u8) -> Option<Self>`
- `CodecId::to_wire(self) -> u8`
- `QualityProfile::total_bitrate_kbps(&self) -> f32`
### Quality Controller (`quality.rs`)
```rust
pub enum Tier { Good, Degraded, Catastrophic }
pub struct AdaptiveQualityController { /* ... */ }
```
Key methods:
- `AdaptiveQualityController::new() -> Self` -- starts at Tier::Good
- `AdaptiveQualityController::tier(&self) -> Tier`
- `Tier::classify(report: &QualityReport) -> Self`
- `Tier::profile(self) -> QualityProfile`
### Jitter Buffer (`jitter.rs`)
```rust
pub struct JitterBuffer { /* ... */ }
pub struct JitterStats { pub packets_received: u64, pub packets_played: u64, pub packets_lost: u64, pub packets_late: u64, pub packets_duplicate: u64, pub current_depth: usize }
pub enum PlayoutResult { Packet(MediaPacket), Missing { seq: u16 }, NotReady }
```
Key methods:
- `JitterBuffer::new(target_depth: usize, max_depth: usize, min_depth: usize) -> Self`
- `JitterBuffer::default_5s() -> Self` -- target=50, max=250, min=25
- `JitterBuffer::push(&mut self, packet: MediaPacket)`
- `JitterBuffer::pop(&mut self) -> PlayoutResult`
- `JitterBuffer::depth(&self) -> usize`
- `JitterBuffer::stats(&self) -> &JitterStats`
- `JitterBuffer::reset(&mut self)`
- `JitterBuffer::set_target_depth(&mut self, depth: usize)`
### Session State Machine (`session.rs`)
```rust
pub enum SessionState { Idle, Connecting, Handshaking, Active, Rekeying, Closed }
pub enum SessionEvent { Initiate, Connected, HandshakeComplete, RekeyStart, RekeyComplete, Terminate{reason}, ConnectionLost }
pub struct Session { /* ... */ }
```
Key methods:
- `Session::new(session_id: [u8; 16]) -> Self`
- `Session::state(&self) -> SessionState`
- `Session::transition(&mut self, event: SessionEvent, now_ms: u64) -> Result<SessionState, TransitionError>`
- `Session::is_media_active(&self) -> bool` -- true for Active and Rekeying
### Error Types (`error.rs`)
```rust
pub enum CodecError { EncodeFailed(String), DecodeFailed(String), UnsupportedTransition{from, to} }
pub enum FecError { BlockFull{max}, InsufficientSymbols{needed, have}, InvalidBlock(u8), Internal(String) }
pub enum CryptoError { DecryptionFailed, InvalidPublicKey, RekeyFailed(String), ReplayDetected{seq}, Internal(String) }
pub enum TransportError { ConnectionLost, DatagramTooLarge{size, max}, Timeout{ms}, Io(io::Error), Internal(String) }
pub enum ObfuscationError { Failed(String), InvalidFraming }
```
### PathQuality (`traits.rs`)
```rust
pub struct PathQuality {
pub loss_pct: f32, // 0.0-100.0
pub rtt_ms: u32,
pub jitter_ms: u32,
pub bandwidth_kbps: u32,
}
```
---
## wzp-codec
**Path**: `crates/wzp-codec/src/`
### Factory Functions (`lib.rs`)
```rust
/// Create an adaptive encoder (accepts 48 kHz PCM, handles resampling for Codec2).
pub fn create_encoder(profile: QualityProfile) -> Box<dyn AudioEncoder>
/// Create an adaptive decoder (outputs 48 kHz PCM, handles upsampling from Codec2).
pub fn create_decoder(profile: QualityProfile) -> Box<dyn AudioDecoder>
```
### Public Types
```rust
pub struct AdaptiveEncoder { /* wraps OpusEncoder + Codec2Encoder */ }
pub struct AdaptiveDecoder { /* wraps OpusDecoder + Codec2Decoder */ }
pub struct OpusEncoder { /* audiopus::coder::Encoder wrapper */ }
pub struct OpusDecoder { /* audiopus::coder::Decoder wrapper */ }
pub struct Codec2Encoder { /* codec2::Codec2 wrapper */ }
pub struct Codec2Decoder { /* codec2::Codec2 wrapper */ }
```
Key methods on concrete types:
- `OpusEncoder::new(profile: QualityProfile) -> Result<Self, CodecError>`
- `OpusEncoder::frame_samples(&self) -> usize` -- 960 for 20ms, 1920 for 40ms
- `Codec2Encoder::new(profile: QualityProfile) -> Result<Self, CodecError>`
- `Codec2Encoder::frame_samples(&self) -> usize` -- 160 for 20ms/3200bps, 320 for 40ms/1200bps
### Resampler (`resample.rs`)
```rust
pub fn resample_48k_to_8k(input: &[i16]) -> Vec<i16> // 6:1 decimation with box filter
pub fn resample_8k_to_48k(input: &[i16]) -> Vec<i16> // 1:6 linear interpolation
```
---
## wzp-fec
**Path**: `crates/wzp-fec/src/`
### Factory Functions (`lib.rs`)
```rust
/// Create an encoder/decoder pair configured for the given quality profile.
pub fn create_fec_pair(profile: &QualityProfile) -> (RaptorQFecEncoder, RaptorQFecDecoder)
/// Create an encoder configured for the given quality profile.
pub fn create_encoder(profile: &QualityProfile) -> RaptorQFecEncoder
/// Create a decoder configured for the given quality profile.
pub fn create_decoder(profile: &QualityProfile) -> RaptorQFecDecoder
```
### RaptorQFecEncoder (`encoder.rs`)
```rust
pub struct RaptorQFecEncoder { /* block_id, frames_per_block, source_symbols, symbol_size */ }
```
Key methods:
- `RaptorQFecEncoder::new(frames_per_block: usize, symbol_size: u16) -> Self`
- `RaptorQFecEncoder::with_defaults(frames_per_block: usize) -> Self` -- symbol_size=256
- Implements `FecEncoder` trait
### RaptorQFecDecoder (`decoder.rs`)
```rust
pub struct RaptorQFecDecoder { /* blocks: HashMap<u8, BlockState>, symbol_size, frames_per_block */ }
```
Key methods:
- `RaptorQFecDecoder::new(frames_per_block: usize, symbol_size: u16) -> Self`
- `RaptorQFecDecoder::with_defaults(frames_per_block: usize) -> Self`
- Implements `FecDecoder` trait
### Interleaver (`interleave.rs`)
```rust
pub type Symbol = (u8, u8, bool, Vec<u8>); // (block_id, symbol_index, is_repair, data)
pub struct Interleaver { depth: usize }
```
Key methods:
- `Interleaver::new(depth: usize) -> Self`
- `Interleaver::with_default_depth() -> Self` -- depth=3
- `Interleaver::interleave(&self, blocks: &[Vec<Symbol>]) -> Vec<Symbol>`
- `Interleaver::depth(&self) -> usize`
### AdaptiveFec (`adaptive.rs`)
```rust
pub struct AdaptiveFec { pub frames_per_block: usize, pub repair_ratio: f32, pub symbol_size: u16 }
```
Key methods:
- `AdaptiveFec::from_profile(profile: &QualityProfile) -> Self`
- `AdaptiveFec::build_encoder(&self) -> RaptorQFecEncoder`
- `AdaptiveFec::ratio(&self) -> f32`
- `AdaptiveFec::overhead_factor(&self) -> f32` -- 1.0 + repair_ratio
### Block Managers (`block_manager.rs`)
```rust
pub enum EncoderBlockState { Building, Pending, Sent, Acknowledged }
pub enum DecoderBlockState { Assembling, Complete, Expired }
pub struct EncoderBlockManager { /* ... */ }
pub struct DecoderBlockManager { /* ... */ }
```
Key methods:
- `EncoderBlockManager::next_block_id(&mut self) -> u8`
- `EncoderBlockManager::mark_sent(&mut self, block_id: u8)`
- `EncoderBlockManager::mark_acknowledged(&mut self, block_id: u8)`
- `DecoderBlockManager::touch(&mut self, block_id: u8)`
- `DecoderBlockManager::mark_complete(&mut self, block_id: u8)`
- `DecoderBlockManager::expire_before(&mut self, block_id: u8)`
### Helper Functions (`encoder.rs`)
```rust
/// Build source EncodingPackets for a given block (for testing/interleaving).
pub fn source_packets_for_block(block_id: u8, symbols: &[Vec<u8>], symbol_size: u16) -> Vec<EncodingPacket>
/// Generate repair packets for the given source symbols.
pub fn repair_packets_for_block(block_id: u8, symbols: &[Vec<u8>], symbol_size: u16, ratio: f32) -> Vec<EncodingPacket>
```
---
## wzp-crypto
**Path**: `crates/wzp-crypto/src/`
### Re-exports (`lib.rs`)
```rust
pub use anti_replay::AntiReplayWindow;
pub use handshake::WarzoneKeyExchange;
pub use nonce::{build_nonce, Direction};
pub use rekey::RekeyManager;
pub use session::ChaChaSession;
pub use wzp_proto::{CryptoError, CryptoSession, KeyExchange};
```
### WarzoneKeyExchange (`handshake.rs`)
```rust
pub struct WarzoneKeyExchange { /* signing_key, x25519_static, ephemeral_secret */ }
```
Implements `KeyExchange` trait. Key derivation:
- Ed25519: `HKDF(seed, "warzone-ed25519-identity")`
- X25519: `HKDF(seed, "warzone-x25519-identity")`
- Session: `HKDF(X25519_DH_shared_secret, "warzone-session-key")`
### ChaChaSession (`session.rs`)
```rust
pub struct ChaChaSession { /* cipher, session_id, send_seq, recv_seq, rekey_mgr, pending_rekey_secret */ }
```
Key methods:
- `ChaChaSession::new(shared_secret: [u8; 32]) -> Self`
- Implements `CryptoSession` trait
### AntiReplayWindow (`anti_replay.rs`)
```rust
pub struct AntiReplayWindow { /* highest: u16, bitmap: Vec<u64>, initialized: bool */ }
```
Key methods:
- `AntiReplayWindow::new() -> Self` -- 1024-packet window
- `AntiReplayWindow::check_and_update(&mut self, seq: u16) -> Result<(), CryptoError>`
### Nonce Construction (`nonce.rs`)
```rust
pub enum Direction { Send = 0, Recv = 1 }
pub fn build_nonce(session_id: &[u8; 4], seq: u32, direction: Direction) -> [u8; 12]
```
### RekeyManager (`rekey.rs`)
```rust
pub struct RekeyManager { /* current_key, last_rekey_at */ }
```
Key methods:
- `RekeyManager::new(initial_key: [u8; 32]) -> Self`
- `RekeyManager::should_rekey(&self, packet_count: u64) -> bool` -- every 2^16 packets
- `RekeyManager::perform_rekey(&mut self, new_peer_pub: &[u8; 32], our_new_secret: StaticSecret, packet_count: u64) -> [u8; 32]`
---
## wzp-transport
**Path**: `crates/wzp-transport/src/`
### Re-exports (`lib.rs`)
```rust
pub use config::{client_config, server_config};
pub use connection::{accept, connect, create_endpoint};
pub use path_monitor::PathMonitor;
pub use quic::QuinnTransport;
pub use wzp_proto::{MediaTransport, PathQuality, TransportError};
```
### QuinnTransport (`quic.rs`)
```rust
pub struct QuinnTransport { /* connection: quinn::Connection, path_monitor: Mutex<PathMonitor> */ }
```
Key methods:
- `QuinnTransport::new(connection: quinn::Connection) -> Self`
- `QuinnTransport::connection(&self) -> &quinn::Connection`
- `QuinnTransport::max_datagram_size(&self) -> Option<usize>`
- Implements `MediaTransport` trait
### Configuration (`config.rs`)
```rust
/// Create a server configuration with a self-signed certificate.
pub fn server_config() -> (quinn::ServerConfig, Vec<u8>)
/// Create a client configuration that trusts any certificate (testing).
pub fn client_config() -> quinn::ClientConfig
```
QUIC parameters: ALPN `wzp`, 30s idle timeout, 5s keepalive, 256KB receive window, 128KB send window, 300ms initial RTT.
### Connection Lifecycle (`connection.rs`)
```rust
pub fn create_endpoint(bind_addr: SocketAddr, server_config: Option<quinn::ServerConfig>) -> Result<quinn::Endpoint, TransportError>
pub async fn connect(endpoint: &quinn::Endpoint, addr: SocketAddr, server_name: &str, config: quinn::ClientConfig) -> Result<quinn::Connection, TransportError>
pub async fn accept(endpoint: &quinn::Endpoint) -> Result<quinn::Connection, TransportError>
```
### PathMonitor (`path_monitor.rs`)
```rust
pub struct PathMonitor { /* EWMA state for loss, RTT, jitter, bandwidth */ }
```
Key methods:
- `PathMonitor::new() -> Self`
- `PathMonitor::observe_sent(&mut self, seq: u16, timestamp_ms: u64)`
- `PathMonitor::observe_received(&mut self, seq: u16, timestamp_ms: u64)`
- `PathMonitor::observe_rtt(&mut self, rtt_ms: u32)`
- `PathMonitor::quality(&self) -> PathQuality`
### Datagram Helpers (`datagram.rs`)
```rust
pub fn serialize_media(packet: &MediaPacket) -> Bytes
pub fn deserialize_media(data: Bytes) -> Option<MediaPacket>
pub fn max_datagram_payload(connection: &quinn::Connection) -> Option<usize>
```
### Reliable Stream Framing (`reliable.rs`)
```rust
pub async fn send_signal(connection: &Connection, msg: &SignalMessage) -> Result<(), TransportError>
pub async fn recv_signal(recv: &mut quinn::RecvStream) -> Result<SignalMessage, TransportError>
```
Framing: 4-byte big-endian length prefix + serde_json payload. Max message size: 1 MB.
---
## wzp-relay
**Path**: `crates/wzp-relay/src/`
### Re-exports (`lib.rs`)
```rust
pub use config::RelayConfig;
pub use handshake::accept_handshake;
pub use pipeline::{PipelineConfig, PipelineStats, RelayPipeline};
pub use session_mgr::{RelaySession, SessionId, SessionManager};
```
### RoomManager (`room.rs`)
```rust
pub type ParticipantId = u64;
pub struct RoomManager { /* rooms: HashMap<String, Room> */ }
```
Key methods:
- `RoomManager::new() -> Self`
- `RoomManager::join(&mut self, room_name: &str, addr: SocketAddr, transport: Arc<QuinnTransport>) -> ParticipantId`
- `RoomManager::leave(&mut self, room_name: &str, participant_id: ParticipantId)`
- `RoomManager::others(&self, room_name: &str, participant_id: ParticipantId) -> Vec<Arc<QuinnTransport>>`
- `RoomManager::room_size(&self, room_name: &str) -> usize`
- `RoomManager::list(&self) -> Vec<(String, usize)>`
```rust
/// Run the receive loop for one participant in a room (forwards to all others).
pub async fn run_participant(room_mgr: Arc<Mutex<RoomManager>>, room_name: String, participant_id: ParticipantId, transport: Arc<QuinnTransport>)
```
### RelayPipeline (`pipeline.rs`)
```rust
pub struct PipelineConfig { pub initial_profile: QualityProfile, pub jitter_target: usize, pub jitter_max: usize, pub jitter_min: usize }
pub struct PipelineStats { pub packets_received: u64, pub packets_forwarded: u64, pub packets_fec_recovered: u64, pub packets_lost: u64, pub profile_changes: u64 }
pub struct RelayPipeline { /* fec_encoder, fec_decoder, jitter, quality, profile, out_seq, stats */ }
```
Key methods:
- `RelayPipeline::new(config: PipelineConfig) -> Self`
- `RelayPipeline::ingest(&mut self, packet: MediaPacket) -> Vec<MediaPacket>` -- FEC decode + jitter pop
- `RelayPipeline::prepare_outbound(&mut self, packet: MediaPacket) -> Vec<MediaPacket>` -- assign seq + FEC encode
- `RelayPipeline::stats(&self) -> &PipelineStats`
- `RelayPipeline::profile(&self) -> QualityProfile`
### SessionManager (`session_mgr.rs`)
```rust
pub type SessionId = [u8; 16];
pub struct RelaySession { pub state: Session, pub upstream_pipeline: RelayPipeline, pub downstream_pipeline: RelayPipeline, pub profile: QualityProfile, pub last_activity_ms: u64 }
pub struct SessionManager { /* sessions: HashMap<SessionId, RelaySession>, max_sessions */ }
```
Key methods:
- `SessionManager::new(max_sessions: usize) -> Self`
- `SessionManager::create_session(&mut self, session_id: SessionId, config: PipelineConfig) -> Option<&mut RelaySession>`
- `SessionManager::get_session(&mut self, id: &SessionId) -> Option<&mut RelaySession>`
- `SessionManager::remove_session(&mut self, id: &SessionId) -> Option<RelaySession>`
- `SessionManager::expire_idle(&mut self, now_ms: u64, timeout_ms: u64) -> usize`
### Handshake (`handshake.rs`)
```rust
/// Accept the relay (callee) side of the cryptographic handshake.
pub async fn accept_handshake(transport: &dyn MediaTransport, seed: &[u8; 32]) -> Result<(Box<dyn CryptoSession>, QualityProfile), anyhow::Error>
```
### RelayConfig (`config.rs`)
```rust
pub struct RelayConfig {
pub listen_addr: SocketAddr, // default: 0.0.0.0:4433
pub remote_relay: Option<SocketAddr>, // None = room mode
pub max_sessions: usize, // default: 100
pub jitter_target_depth: usize, // default: 50
pub jitter_max_depth: usize, // default: 250
pub log_level: String, // default: "info"
}
```
---
## wzp-client
**Path**: `crates/wzp-client/src/`
### Re-exports (`lib.rs`)
```rust
#[cfg(feature = "audio")]
pub use audio_io::{AudioCapture, AudioPlayback};
pub use call::{CallConfig, CallDecoder, CallEncoder};
pub use handshake::perform_handshake;
```
### CallEncoder (`call.rs`)
```rust
pub struct CallEncoder { /* audio_enc, fec_enc, profile, seq, block_id, frame_in_block, timestamp_ms */ }
```
Key methods:
- `CallEncoder::new(config: &CallConfig) -> Self`
- `CallEncoder::encode_frame(&mut self, pcm: &[i16]) -> Result<Vec<MediaPacket>, anyhow::Error>` -- returns source + repair packets
- `CallEncoder::set_profile(&mut self, profile: QualityProfile) -> Result<(), anyhow::Error>`
### CallDecoder (`call.rs`)
```rust
pub struct CallDecoder { /* audio_dec, fec_dec, jitter, quality, profile */ }
```
Key methods:
- `CallDecoder::new(config: &CallConfig) -> Self`
- `CallDecoder::ingest(&mut self, packet: MediaPacket)` -- feeds FEC decoder and jitter buffer
- `CallDecoder::decode_next(&mut self, pcm: &mut [i16]) -> Option<usize>` -- pops from jitter, decodes
- `CallDecoder::profile(&self) -> QualityProfile`
- `CallDecoder::jitter_stats(&self) -> JitterStats`
### CallConfig (`call.rs`)
```rust
pub struct CallConfig {
pub profile: QualityProfile, // default: GOOD
pub jitter_target: usize, // default: 10
pub jitter_max: usize, // default: 250
pub jitter_min: usize, // default: 3
}
```
### Client Handshake (`handshake.rs`)
```rust
/// Perform the client (caller) side of the cryptographic handshake.
pub async fn perform_handshake(transport: &dyn MediaTransport, seed: &[u8; 32]) -> Result<Box<dyn CryptoSession>, anyhow::Error>
```
### Echo Test (`echo_test.rs`)
```rust
pub struct WindowResult { pub index: usize, pub time_offset_secs: f64, pub frames_sent: u32, pub frames_received: u32, pub loss_pct: f32, pub snr_db: f32, pub correlation: f32, pub peak_amplitude: i16, pub is_silent: bool }
pub struct EchoTestResult { pub duration_secs: f64, pub total_frames_sent: u64, pub total_frames_received: u64, pub overall_loss_pct: f32, pub windows: Vec<WindowResult>, /* ... */ }
pub async fn run_echo_test(transport: &(dyn MediaTransport + Send + Sync), duration_secs: u32, window_secs: f64) -> anyhow::Result<EchoTestResult>
pub fn print_report(result: &EchoTestResult)
```
### Audio I/O (`audio_io.rs`, requires `audio` feature)
```rust
pub struct AudioCapture { /* rx: mpsc::Receiver<Vec<i16>>, running: Arc<AtomicBool> */ }
pub struct AudioPlayback { /* tx: mpsc::SyncSender<Vec<i16>>, running: Arc<AtomicBool> */ }
```
Key methods:
- `AudioCapture::start() -> Result<Self, anyhow::Error>` -- opens default input at 48 kHz mono
- `AudioCapture::read_frame(&self) -> Option<Vec<i16>>` -- blocking, returns 960 samples
- `AudioCapture::stop(&self)`
- `AudioPlayback::start() -> Result<Self, anyhow::Error>` -- opens default output at 48 kHz mono
- `AudioPlayback::write_frame(&self, pcm: &[i16])`
- `AudioPlayback::stop(&self)`
### Benchmarks (`bench.rs`)
```rust
pub struct CodecResult { pub frames: usize, pub avg_encode_us: f64, pub avg_decode_us: f64, pub frames_per_sec: f64, pub compression_ratio: f64, /* ... */ }
pub struct FecResult { pub blocks_attempted: usize, pub blocks_recovered: usize, pub recovery_rate_pct: f64, /* ... */ }
pub struct CryptoResult { pub packets: usize, pub packets_per_sec: f64, pub megabytes_per_sec: f64, pub avg_latency_us: f64, /* ... */ }
pub struct PipelineResult { pub frames: usize, pub avg_e2e_latency_us: f64, pub overhead_ratio: f64, /* ... */ }
pub fn generate_sine_wave(freq_hz: f32, sample_rate: u32, num_samples: usize) -> Vec<i16>
pub fn bench_codec_roundtrip() -> CodecResult // 1000 frames Opus 24kbps
pub fn bench_fec_recovery(loss_pct: f32) -> FecResult // 100 blocks with simulated loss
pub fn bench_encrypt_decrypt() -> CryptoResult // 30000 packets ChaCha20
pub fn bench_full_pipeline() -> PipelineResult // 50 frames E2E
```
---
## wzp-web
**Path**: `crates/wzp-web/src/`
The web bridge binary. No public library API -- it is a standalone Axum server.
### Binary: `wzp-web`
- Serves static files from `crates/wzp-web/static/`
- WebSocket endpoint: `GET /ws/{room}` -- upgrades to WebSocket
- Each WebSocket client gets a QUIC connection to the relay with the room name as SNI
- Browser -> relay: WebSocket binary messages (960 Int16 samples as raw bytes) -> `CallEncoder` -> `MediaTransport::send_media()`
- Relay -> browser: `MediaTransport::recv_media()` -> `CallDecoder` -> WebSocket binary messages
### Static Files
- `static/index.html` -- web UI with room input, connect/disconnect, PTT, level meter
- `static/audio-processor.js` -- AudioWorklet for microphone capture (960-sample frames)
- `static/playback-processor.js` -- AudioWorklet for audio playback (ring buffer, 200ms max)

View File

@@ -0,0 +1,752 @@
---
tags: [reference, wzp]
type: reference
---
# 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) |
| `--event-log <path>` | Write JSONL protocol event log for federation debugging |
| `--version`, `-V` | Print build git hash and exit |
| `--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
### Event Log (Protocol Analyzer)
Use `--event-log` to write a JSONL event log that traces every federation media packet through the relay pipeline. Essential for debugging federation audio issues.
```bash
wzp-relay --config relay.toml --event-log /tmp/events.jsonl
```
Each media packet emits events at every decision point:
- `federation_ingress` — packet arrived from a peer relay
- `local_deliver` — packet delivered to local participants
- `dedup_drop` — packet dropped as duplicate
- `rate_limit_drop` — packet dropped by rate limiter
- `room_not_found` — packet for unknown room
- `local_deliver_error` — delivery to local client failed
Analyze with:
```bash
# Count events by type
cat events.jsonl | python3 -c "
import json, collections, sys
c = collections.Counter()
for l in sys.stdin: c[json.loads(l)['event']] += 1
for k,v in sorted(c.items(), key=lambda x:-x[1]): print(f' {k}: {v}')
"
```
### Remote Version Check
Verify a deployed relay's version without SSH:
```bash
wzp-client --version-check <relay-addr:port>
```
### 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 |
| Federation audio silent on consecutive connects | Dedup filter or jitter buffer state | Verify relay is running latest build with time-based dedup |
| Federation participant shows wrong relay label | Hub relay not propagating original labels | Update relay to latest build (label preservation fix) |
| Federation disconnect takes >15 seconds | QUIC idle timeout + stale sweeper | Normal: sweeper runs every 5s with 15s TTL. Use latest client with SIGTERM handler for instant disconnect |
| 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
```
## Build Pipelines
All production artifacts (Android APK, Linux x86_64 binaries, Windows `.exe`) are built on **SepehrHomeserverdk** using Docker, not on developer workstations. The pipelines are fire-and-forget: a local script invokes a `tmux` session on the remote, the build runs in a Docker container, and the artifact is uploaded to `paste.dk.manko.yoga` (rustypaste) with a notification sent to `ntfy.sh/wzp` on start and completion.
### Docker images
Two long-lived images live on the remote:
| Image | Used by | Base | Key contents |
|---|---|---|---|
| `wzp-android-builder` | Android APK (Tauri mobile + legacy Kotlin), Linux x86_64 relay/CLI | Debian bookworm | Rust stable with Android targets, cargo-ndk, NDK 26.1, Android SDK (API 34 + 35 + 36), JDK 17, Gradle 8.5, Node.js 20, cmake, ninja, tauri-cli 2.x |
| `wzp-windows-builder` | Windows x86_64 `.exe` | Debian bookworm | Rust stable with `x86_64-pc-windows-msvc` target, cargo-xwin (with pre-warmed MSVC CRT + Windows SDK cache), Node.js 20, cmake, ninja, clang, lld, nasm |
Both images are rebuilt rarely — once the base toolchain is stable, rebuilds are only needed to pick up new dependencies or security patches.
**Rebuilding an image** (fire-and-forget, ~10 min on a warm base):
```bash
# Windows
./scripts/build-windows-docker.sh --image-build
# Android (upload and rebuild handled by the Android build script itself — see
# its --image-build flag or equivalent)
```
The `--image-build` flag uploads the local Dockerfile to the remote, kicks off `docker build` under `nohup`, and returns immediately. Monitor with:
```bash
ssh SepehrHomeserverdk 'tail -f /tmp/wzp-windows-image-build.log'
```
### Pipeline: Android APK (Tauri Mobile)
```bash
./scripts/build-tauri-android.sh # Full: pull + build + upload + notify
./scripts/build-tauri-android.sh --no-pull # Skip git fetch
./scripts/build-tauri-android.sh --clean # Force-clean Rust target
```
- **Branch**: `android-rewrite`
- **Image**: `wzp-android-builder`
- **Build command**: `cargo tauri android build --release`
- **Output**: `wzp-release.apk` → uploaded to rustypaste
- **Notifications**: start + completion to `ntfy.sh/wzp`
- **Remote artifact path**: `/mnt/storage/manBuilder/data/cache-android/target/…/release/app-release.apk`
### Pipeline: Linux x86_64 (relay + CLI + bench + web)
```bash
./scripts/build-linux-docker.sh # Fire-and-forget
./scripts/build-linux-docker.sh --no-pull # Skip git fetch
./scripts/build-linux-docker.sh --clean # Force-clean target
./scripts/build-linux-docker.sh --install # Wait for completion and download locally
```
- **Branch**: `feat/android-voip-client` (script default — override by editing the script or passing an env var)
- **Image**: `wzp-android-builder` (shared, not a separate Linux-only image)
- **Targets built**: `wzp-relay`, `wzp-client`, `wzp-client-audio` (with `--features audio`), `wzp-web`, `wzp-bench`
- **Output**: `wzp-linux-x86_64.tar.gz` with all five binaries → uploaded to rustypaste
- **Local landing dir** (with `--install`): `target/linux-x86_64/`
### Pipeline: Windows x86_64 (`wzp-desktop.exe`)
```bash
./scripts/build-windows-docker.sh # Full: pull + build + download locally
./scripts/build-windows-docker.sh --no-pull # Skip git fetch
./scripts/build-windows-docker.sh --rust # Force-clean target-windows cache
./scripts/build-windows-docker.sh --image-build # Rebuild the Docker image (fire-and-forget)
```
- **Branch**: `feat/desktop-audio-rewrite`
- **Image**: `wzp-windows-builder`
- **Build command**: `cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop`
- **Output**: `wzp-desktop.exe` (~16 MB) → downloaded to `target/windows-exe/wzp-desktop.exe`, also uploaded to rustypaste
- **Target cache volume**: `target-windows` (separate from the Android target cache to avoid triple cross-contamination)
- **Shared cache volumes**: `cargo-registry`, `cargo-git` (shared with Android — both pipelines pull the same crates)
**A/B-preserving workflow** for testing audio backends: rename the prior `.exe` before re-running the build, so both coexist:
```bash
# Preserve prior build as the noAEC baseline
mv target/windows-exe/wzp-desktop.exe target/windows-exe/wzp-desktop-noAEC.exe
./scripts/build-windows-docker.sh
ls -la target/windows-exe/
# wzp-desktop-noAEC.exe (previous build)
# wzp-desktop.exe (new build)
```
### Alternative pipeline: Windows via Hetzner Cloud VPS
For situations where Docker image rebuilds would be disruptive, or for one-shot debug builds on a clean machine:
```bash
./scripts/build-windows-cloud.sh # Full: create VM → build → download → destroy
./scripts/build-windows-cloud.sh --prepare # Create VM + install deps, don't build
./scripts/build-windows-cloud.sh --build # Build on existing VM
./scripts/build-windows-cloud.sh --transfer # Download .exe from existing VM
./scripts/build-windows-cloud.sh --destroy # Delete the VM
WZP_KEEP_VM=1 ./scripts/build-windows-cloud.sh # Don't auto-destroy after successful build
```
- **Provider**: Hetzner Cloud
- **Default server type**: `cx33` (8 GB RAM, 8 vCPU — `cx23` with 4 GB OOMs on the tauri+rustls cross-compile)
- **Image**: `ubuntu-24.04`
- **SSH key**: must be named `wz` in Hetzner and loaded in the local ssh-agent
- **Reminder**: set `WZP_KEEP_VM=1` for multi-build sessions, then **remember to `--destroy` at end of day** so the VM isn't left running overnight. This is tracked in the auto-memory as `feedback_keep_windows_builder_vm.md`.
### Notifications
All pipelines post to `https://ntfy.sh/wzp`. Subscribe from your phone via the [ntfy.sh app](https://ntfy.sh/) to get push notifications on build start/success/failure. Messages include the short git hash and the rustypaste URL on success:
```
WZP Windows build OK [03a80a3] (16M)
https://paste.dk.manko.yoga/<uuid>/wzp-desktop.exe
```
### Rustypaste credentials
Build pipelines read `rusty_address` and `rusty_auth_token` from the `.env` file at `/mnt/storage/manBuilder/.env` on SepehrHomeserverdk. Local scripts that upload directly (`build-windows-cloud.sh` when run in `--transfer` mode) read from `~/.wzp/rustypaste.env` with the same variable names. Both files must be kept in sync manually if rotated.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
---
tags: [reference, wzp]
type: reference
---
# FeatherChat: Voice/Video Calling Integration with Warzone Messenger
## Overview
Voice/video calling system designed to integrate with the existing E2E encrypted Warzone messenger. Reuses the same identity, addressing, and key exchange infrastructure.
## Identity Model (reuse, not duplicate)
- **Identity**: 32-byte seed derives both keypairs via HKDF:
- Ed25519 (signing)
- X25519 (encryption)
- **Fingerprint**: `SHA-256(Ed25519 public key)[:16]`, displayed as `xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx`
- **Backup**: BIP39 mnemonic (24 words) for seed recovery
- **Storage**: Seed encrypted at rest with Argon2id + ChaCha20-Poly1305
- **Future**: Ethereum address as fingerprint (secp256k1 derived from same BIP39 seed)
## Addressing (reuse)
| Method | Format | Resolution |
|--------|--------|------------|
| Local alias | `@manwe` | Server resolves to fingerprint |
| Federated | `@manwe.b1.example.com` | DNS TXT record → fingerprint + server endpoint |
| ENS | `@manwe.eth` | Ethereum address → fingerprint (Phase 2-3) |
| Raw fingerprint | `xxxx:xxxx:...` | Direct lookup (always works as fallback) |
## Key Exchange (can extend)
- **X3DH** for session establishment:
- Ed25519 identity key
- X25519 ephemeral key
- Signed pre-keys
- **Double Ratchet** for forward secrecy on data channels
- **Pre-key bundles** stored on server, fetched by callers
## Server Infrastructure
- **Stack**: Rust (axum), sled DB, WebSocket for real-time
- **Trust model**: Server is untrusted relay — never sees plaintext
- **Groups**: Named, auto-created, per-member encryption
- **Federation**: Via DNS TXT records (Phase 3)
## Calling System Requirements
1. **Signaling**: Reuse existing WebSocket connection and identity
2. **Key derivation**: SRTP/DTLS keys derived from existing X3DH shared secret (or new ephemeral exchange per call)
3. **Call initiation**: `WireMessage::CallOffer`, `CallAnswer`, `CallIceCandidate` variants
4. **NAT traversal**: STUN/TURN server integration
5. **Group calls**: SFU (Selective Forwarding Unit) vs mesh topology for up to 50 users
6. **Codecs**: Opus for audio, VP8/VP9/AV1 for video
7. **E2E media encryption**: Insertable streams API (WebRTC) or custom SRTP
8. **Unified addressing**: A user calls `@manwe` the same way they message `@manwe`
## Degradation Strategy
Calls should degrade gracefully under unreliable/warzone network conditions:
```
Video (full) → Video (low res) → Audio (high quality) → Audio (low bitrate)
```
- Support opportunistic cooperation
- Fall back to TURN/TCP through the existing WebSocket when UDP is blocked

View File

@@ -0,0 +1,171 @@
---
tags: [reference, wzp]
type: reference
---
# Handoff — 2026-05-12 EOD
## TL;DR
Wave 5 (Phase 5) and Wave 6 (Phase 6) implementation is complete and approved on the board. Stopping for the night with one open issue: `wzp-video` does not target-compile for `aarch64-linux-android` and needs a focused `ndk = "0.9"` API migration session (~12 h). Nothing live is blocked — Tauri Android does not yet consume `wzp-video`.
**Branch state:** local `experimental-ui` HEAD `f3e3ee5`, pushed to `github` only. **Not yet on `fj`** (deploy key was read-only). Build server (`manwe@manwehs`) is up to date via github fetch.
---
## What landed today
| Wave | Tasks approved | New crates / files | Test delta |
|---|---|---|---|
| 5 | T5.1, T5.1.1, T5.2, T5.3, T5.4, T5.5, T5.6, T5.7, T5.7.1, T5.8 | `crates/wzp-relay/src/audio_scorer.rs`, `response_policy.rs`, `verdict.rs`; `wzp-video/src/controller.rs`, `simulcast.rs`, `encoder_mode.rs`; H.265 path in VT + MediaCodec | wzp-relay 99→127, wzp-video 43→71 |
| 6 | T6.1 (+ rework), T6.1.2, T6.2 | `wzp-video/src/av1_obu.rs`, `dav1d.rs`, `svt_av1.rs`, `factory.rs`; VT AV1 decoder; MediaCodec AV1; `wzp-relay/src/video_scorer.rs` | wzp-video 76→88, wzp-relay 127→137 |
Total: ~30 task units approved across the two waves. Workspace tests at 702 passing (excluding `wzp-android`).
---
## Open / next-up
### Top of queue
- **T4.3.1.1 (deferred → in-progress, blocked)** — Android target-compile of `wzp-video`. We started this tonight and hit 31 errors in `crates/wzp-video/src/mediacodec.rs` against the actual `ndk = "0.9"` API. Error categories captured below; resume with one fix-per-category commit, then attempt device instrumentation.
- **T6.3 — federated reputation gossip.** Design exploration committed (`1e729e4`, `docs/PRD/PRD-relay-federation-gossip.md`). **Decision made: Approach 3 (Ban-List Distribution).** My answers to the 6 blocker questions are in the chat thread, awaiting conversion to a real Files/Steps/Verify/Done-when task spec for the agent. The user opted not to run the agent immediately; the task spec is a write-then-park.
- **T5.1.1 follow-ups** — none. T5.1.1 closed clean.
### Latent follow-ups from earlier waves
These pre-date wave 6 and are still open:
- **AEAD wired into prod send/recv path** (referenced in T1.5 / T1.6 reports). Encryption is implemented in `wzp-crypto` but not yet on every QUIC datagram path.
- **AEAD nonce derivation: switch to `MediaHeader::seq`** (cited in T1.5.x reports). Current scheme works but isn't tied to wire-level seq.
- **`wzp-codec` clippy debt sprint** — 9 errors documented as known debt in `docs/PROTOCOL-AUDIT.md`.
- **T6.1.2 — wire AV1 into actual call engine.** The factory + step tables landed (commit `086d0a4`); no caller invokes `create_video_encoder(Av1Main, …)` yet. Real video sender wiring (the originally-blocked task) is unstarted.
- **T6.2-follow-up — wire `VideoScorer::observe()` into the packet path.** TODO marker at `crates/wzp-relay/src/room.rs:1263`.
### Permanently deferred
- **T6.1.1 — Android MediaCodec AV1 device validation.** Deferred indefinitely: the user does not own an AV1-encode-capable Android or iPhone, and AV1 hardware will not be widespread for years. Revisit when devices land.
---
## The T4.3.1.1 Android build situation
What we did tonight:
1. Pushed `experimental-ui` to `github` (deploy key on `fj` is read-only).
2. Added `github` as a remote on `manwe@manwehs:~/wzp-builder/data/source/` and checked out `experimental-ui`.
3. Ran `cargo build --target aarch64-linux-android -p wzp-video` inside the `wzp-android-builder:latest` docker image.
4. First failure: `shiguredo_dav1d` and `shiguredo_svt_av1` build scripts panic with `unsupported target: os=android, arch=aarch64`. Fixed in commit `f3e3ee5` (`fix(wzp-video): cfg-gate dav1d + svt-av1 off Android target`) — those crates now live under `[target.'cfg(not(target_os = "android"))'.dependencies]`, since Android uses MediaCodec for AV1 anyway.
5. Re-ran the build → 31 errors in `mediacodec.rs`. **Stopped here.**
### Error categories to fix tomorrow
Run the same docker invocation and tackle these one fix-commit per category:
| Error | Count | Root cause | Likely fix |
|---|---|---|---|
| `E0277` `NonNull<AMediaCodec>` not `Send` | ~3 | Raw pointer field on a struct held across `tokio::spawn`-able boundaries | Wrap in `struct SendMediaCodec(NonNull<…>); unsafe impl Send for SendMediaCodec {}` or use the `ndk` crate's owned `MediaCodec` type which already implements `Send` |
| `E0308` `&[MaybeUninit<u8>]` vs `&[u8]` | many | `ndk 0.9` returns uninitialized buffer slices; agent wrote into them as if initialized | Use `MaybeUninit::write_slice` or transmute pattern; pattern matches what `InputBuffer::write` expects |
| `E0425` missing `BITRATE_MODE_CBR` | 1+ | Constant moved/renamed in `ndk 0.9` | Search `ndk` crate docs for current constant name (likely under `MediaCodec::set_parameters` enum) |
| `E0433` `ndk_sys` not linked | several | Agent imported `ndk_sys` directly; it's not a dep, only `ndk = "0.9"` is | Replace direct `ndk_sys` calls with safe wrappers from the `ndk` crate, or add `ndk_sys` as an explicit dep |
| `E0599` `InputBuffer::index()` / `OutputBuffer::index()` private | 2 | Both are private fields in `ndk 0.9`; were public methods in older versions | Either use the buffer through its safe API (queue/dequeue by handle) or expose index via a different accessor — read the `ndk` source for current API |
### Reproduce the build
```bash
ssh -i ~/CascadeProjects/wzp manwe@manwehs \
'cd ~/wzp-builder/data/source && \
docker run --rm \
-v ~/wzp-builder/data/source:/build/source \
-v ~/wzp-builder/data/cache/cargo-registry:/home/builder/.cargo/registry \
-v ~/wzp-builder/data/cache/cargo-git:/home/builder/.cargo/git \
-v ~/wzp-builder/data/cache/target:/build/source/target \
wzp-android-builder:latest \
bash -c "cd /build/source && cargo build --target aarch64-linux-android -p wzp-video 2>&1 | tail -100"'
```
After local fixes:
```bash
git push github experimental-ui && \
ssh -i ~/CascadeProjects/wzp manwe@manwehs \
'cd ~/wzp-builder/data/source && git fetch github && git reset --hard github/experimental-ui'
# then re-run the docker build
```
### Device instrumentation half (post-compile)
User has a physical Android device. Once `cargo build --target aarch64-linux-android -p wzp-video` is clean:
- Build a minimal test harness binary (probably under `wzp-video/examples/` or a new `wzp-android-test/` crate) that does encode → decode of a synthetic frame via MediaCodec.
- Use `adb push` and `adb shell run` to exercise it.
- Compare output bytes against the dav1d/SVT-AV1 SW roundtrip from `crates/wzp-video/src/svt_av1.rs:101 svt_av1_dav1d_roundtrip_10_frames`.
Out of scope for tomorrow if the API migration eats the whole session.
---
## T6.3 — Approach 3 decision
User picked Approach 3 (Ban-List Distribution) from `docs/PRD/PRD-relay-federation-gossip.md`. My answers to the 6 open questions:
1. **Trust model:** Single admin key (user). Strongest Sybil resistance, lowest complexity.
2. **Key infra:** Reuse `wzp-crypto` Ed25519. Admin pubkey in relay config; relays verify list signatures.
3. **Fingerprint scope:** Ed25519 pubkey, not IP. Resistant to NAT rebind evasion.
4. **Privacy:** Publish `SHA-256(pubkey)` hashes, not raw pubkeys. Relays compute `H(observed)` and match. 256-bit space makes brute-force infeasible; loses some audit trail.
5. **TTL:** 30-day per-entry auto-expiry. Forces ops to actively re-publish persistent bans; prevents forever-by-mistake.
6. **Rate limiting:** N/A under Approach 3 (no gossip channel; relays poll a signed list at configurable interval, that interval is the rate limit).
Next step: turn these into a Files/Steps/Verify/Done-when task spec in `docs/PRD/TASKS.md` and move T6.3 from `Blocked``Open` ready for the agent to claim. User did not want this kicked off tonight.
---
## Build / sync state
| Location | Branch | HEAD |
|---|---|---|
| Local (Mac) | `experimental-ui` | `f3e3ee5 fix(wzp-video): cfg-gate dav1d + svt-av1 off Android target` |
| `github` remote | `experimental-ui` | `f3e3ee5` (pushed) |
| `fj` remote | `experimental-ui` | **not pushed** (deploy key read-only on `fj`) |
| `origin` (git.manko.yoga) | `experimental-ui` | **not pushed** |
| Build server `~/wzp-builder/data/source` | `experimental-ui` | `f3e3ee5` |
If you want everything on `fj` / `origin` too, get the deploy key write-privileged or push from a different identity.
`fj/main` and `github/main` have one commit (`9ae9441 fix(audio): check capture ring available...`) that doesn't exist on `experimental-ui` — a small audio fix from May 11. Cherry-pick or merge before merging `experimental-ui` back into `main`.
### Gitleaks allowlist
Added `.gitleaks.toml` in commit `f28f39d` to allowlist 4 pre-existing historical findings. Two are real tokens (paste.tbs.amn.gg and paste.dk.manko.yoga `Authorization` headers in `scripts/build*.sh`). **Rotate those tokens if those endpoints still authenticate** — the allowlist only silences the pre-push hook; the secrets are still in git history.
---
## Agent process notes for tomorrow
The Kimi Code CLI agent on this project has a **stable, well-documented fabrication tic** — one verifiable detail per report is wrong (SHA, "updated X in same commit", fmt/clippy passes, etc.). Pattern survived an explicit CR on T6.1.
**Updated policy** (in `memory/feedback_kimi_report_fabrication.md`):
1. **Always verify the SHA** in the report header against `git log`.
2. **Always run** `cargo fmt --check` and `cargo clippy -- -D warnings` yourself — don't trust the report's claims.
3. **Don't CR fabrications anymore** — the T6.1 CR didn't change the behavior. Reviewer-fix the detail, note on the board, move on. Reserve CRs for substance issues.
The substance of the code has been consistently good. Don't let the fabrication tic bias review of the code itself.
### Rebase tic
Agent has twice rewritten already-pushed commits to address CR feedback (T5.7.1 `d3b2da6``517d0eb`; T6.1 `0de9522``9334aa5`). Forward fix commits are the rule; rebasing wasn't asked for and breaks reviewer references. Mention this only if it happens a third time.
---
## Tomorrow's suggested checklist
1. **(20 min)** Read this doc, the `feedback_kimi_report_fabrication.md` memory, and the T6.1 / T6.2 / T6.1.2 board rows on `docs/PRD/TASKS.md` to reload context.
2. **(12 h)** Resume T4.3.1.1: ndk-0.9 API migration in `crates/wzp-video/src/mediacodec.rs`. One commit per error category.
3. **(30 min)** If migration lands clean, attempt the minimal device test on the user's Android phone.
4. **(20 min, optional)** Convert the T6.3 design answers into a task spec block in `TASKS.md`, leave it `Open` for the agent. Don't kick off the agent unless asked.
5. **(parking lot)** AEAD prod wiring + nonce switch + wzp-codec clippy sprint — none urgent.
---
*Generated 2026-05-12, end of Wave 6 push.*

View File

@@ -0,0 +1,98 @@
---
tags: [reference, wzp]
type: reference
---
# WZP Integration Tasks
Based on featherChat commit 65f6390 — FUTURE_TASKS.md with WZP integration items.
## Status Key
- DONE = implemented and tested
- PARTIAL = code exists but not wired into live path
- TODO = not started
---
## WZP-Side Tasks (our responsibility)
### WZP-S-1. HKDF Salt/Info String Alignment — DONE
- Both use `None` salt, info strings `warzone-ed25519` / `warzone-x25519`
- 15 cross-project tests verify identical output
### WZP-S-2. Accept featherChat Bearer Token on Relay — DONE
- `--auth-url` flag on relay
- Clients send `SignalMessage::AuthToken` as first signal
- Relay calls `POST {auth_url}` to validate, rejects if invalid
- Commit: `ad16ddb`
### WZP-S-3. Signaling Bridge Mode — DONE
- `featherchat.rs` module: encode/decode WZP SignalMessage into FC CallSignal.payload
- `WzpCallPayload` wraps signal + relay_addr + room
- Commit: `ad16ddb`
### WZP-S-4. Room Access Control — DONE
- `hash_room_name()` in wzp-crypto: SHA-256("featherchat-group:" + name)[:16] → 32 hex chars
- CLI `--room <name>` hashes before using as SNI
- Web bridge hashes room name before connecting to relay
- RoomManager gains ACL: `with_acl()`, `allow()`, `is_authorized()`
- `join()` now returns `Result<ParticipantId, String>`, rejects unauthorized
- Relay passes authenticated fingerprint to room join
### WZP-S-5. Wire Crypto Handshake into Live Path — DONE
- CLI: `perform_handshake()` called after connect, before any media mode
- Relay: `accept_handshake()` called after auth, before room join
- Web bridge: `perform_handshake()` called after auth token, before audio loops
- Relay generates ephemeral identity seed at startup, logs fingerprint
- Quality profile negotiated during handshake
### WZP-S-6. Web Bridge + featherChat Web Client — DONE
- `--auth-url` flag on web bridge
- Browser sends `{ "type": "auth", "token": "..." }` as first WS message
- Web bridge validates token against featherChat, then passes to relay
- `--cert`/`--key` flags for production TLS certificates
### WZP-S-7. Publish wzp-proto for featherChat — DONE
- `wzp-proto/Cargo.toml` now standalone (no workspace inheritance)
- featherChat can use: `wzp-proto = { git = "ssh://...", path = "crates/wzp-proto" }`
### WZP-S-8. CLI Seed Input — DONE
- `--seed <hex>` and `--mnemonic <24 words>` flags
- featherChat-compatible identity: same seed → same keys
- Commit: `12cdfe6`
### WZP-S-9. Fix Hardcoded Assumptions — DONE
1. No auth on relay — ✅ fixed via S-2 (`--auth-url`)
2. Room names from SNI — ✅ fixed via S-4 (hashed room names)
3. No signaling before media — ✅ fixed via S-5 (mandatory handshake)
4. Self-signed TLS — ✅ fixed via S-6 (`--cert`/`--key` for production)
5. No codec negotiation in web bridge — ✅ profile negotiated in handshake
6. No connection to FC key registry — ✅ fixed via S-2 (token validation)
---
## featherChat-Side Tasks (their responsibility, we support)
### WZP-FC-1. Add CallSignal WireMessage variant — DONE (v0.0.21, 064a730)
### WZP-FC-2. Call state management + sled tree — TODO (1-2d)
### WZP-FC-3. WS handler for call signaling — TODO (0.5d)
### WZP-FC-4. Auth token validation endpoint — DONE (v0.0.21, 064a730)
### WZP-FC-5. Group-to-room mapping — TODO (1d)
### WZP-FC-6. Presence/online status API — TODO (0.5-2d)
### WZP-FC-7. Missed call notifications — TODO (0.5d)
### WZP-FC-8. Cross-project identity verification — DONE (15 tests, 26dc848)
### WZP-FC-9. HKDF salt investigation — DONE (no mismatch)
### WZP-FC-10. Web bridge shared auth — DONE
- FC: GET /v1/wzp/relay-config, CORS layer, service token
- WZP: web bridge --auth-url validates browser tokens via FC
### FC-CRATE-1. Standalone warzone-protocol — DONE (v0.0.21, 4a4fa9f)
---
## All WZP-S Tasks Complete
The WZP side of integration is finished. featherChat needs:
1. **FC-2 + FC-3** — call state management + WS routing (makes real calls possible)
2. **FC-5** — group-to-room mapping (uses `hash_room_name` convention)
3. **FC-6/7** — presence + missed calls (UX polish)
4. **FC-10** — web bridge shared auth (browser token flow)

500
vault/Reference/Progress.md Normal file
View File

@@ -0,0 +1,500 @@
---
tags: [reference, wzp]
type: reference
---
# WarzonePhone Development Progress Report
## Phase 1: Protocol Core
**Scope**: Define the protocol types, traits, and core logic in `wzp-proto`.
**What was built**:
- Wire format types: `MediaHeader` (12-byte compact binary), `QualityReport` (4 bytes), `MediaPacket`, `SignalMessage` (8 variants)
- Trait definitions: `AudioEncoder`, `AudioDecoder`, `FecEncoder`, `FecDecoder`, `CryptoSession`, `KeyExchange`, `MediaTransport`, `ObfuscationLayer`, `QualityController`
- `CodecId` enum with 5 variants (Opus24k/16k/6k, Codec2_3200/1200) and 4-bit wire encoding
- `QualityProfile` with 3 preset tiers (GOOD, DEGRADED, CATASTROPHIC)
- `AdaptiveQualityController` with hysteresis (3-down/10-up thresholds, sliding window of 20 reports)
- `JitterBuffer` with BTreeMap-based reordering, wrapping sequence arithmetic, min/max/target depth
- `Session` state machine (Idle -> Connecting -> Handshaking -> Active <-> Rekeying -> Closed)
- Full error type hierarchy (`CodecError`, `FecError`, `CryptoError`, `TransportError`, `ObfuscationError`)
**Tests**: 27 tests across packet roundtrip, quality controller, jitter buffer, session state machine, sequence wrapping
## Phase 2: Implementation Crates (Parallel)
**Scope**: Implement the 4 leaf crates against the trait interfaces, in parallel.
### wzp-codec
- Opus encoder/decoder via `audiopus` (48 kHz mono, VoIP application mode, inband FEC, DTX)
- Codec2 encoder/decoder via pure-Rust `codec2` crate (3200 and 1200 bps modes)
- `AdaptiveEncoder`/`AdaptiveDecoder` wrapping both codecs with transparent switching
- Linear resampler for 48 kHz <-> 8 kHz conversion (box filter downsampling, linear interpolation upsampling)
- All callers work with 48 kHz PCM regardless of active codec
### wzp-fec
- `RaptorQFecEncoder`: accumulates source symbols with 2-byte length prefix + zero padding to 256-byte symbol size
- `RaptorQFecDecoder`: multi-block concurrent decoding with HashMap-based block tracking
- `Interleaver`: round-robin temporal interleaving across multiple FEC blocks
- `BlockManager`: encoder-side (Building/Pending/Sent/Acknowledged) and decoder-side (Assembling/Complete/Expired) lifecycle tracking
- `AdaptiveFec`: maps `QualityProfile` to FEC parameters
- Factory function `create_fec_pair()` for convenient encoder/decoder creation
### wzp-crypto
- `WarzoneKeyExchange`: identity seed -> HKDF -> Ed25519 + X25519, ephemeral generation, signature, verification, session derivation
- `ChaChaSession`: ChaCha20-Poly1305 AEAD with deterministic nonce construction (session_id + seq + direction)
- `RekeyManager`: triggers rekey every 2^16 packets, HKDF mixing of old key + new DH, zeroization of old key
- `AntiReplayWindow`: 1024-packet sliding window bitmap with u16 wrapping support
- Nonce module: 12-byte nonce layout (4-byte session_id + 4-byte seq BE + 1-byte direction + 3-byte padding)
### wzp-transport
- `QuinnTransport`: implements `MediaTransport` trait over quinn QUIC connection
- DATAGRAM frames for unreliable media, bidirectional streams for reliable signaling
- Length-prefixed JSON framing (4-byte BE length + serde_json payload) for signaling
- VoIP-tuned QUIC configuration (30s idle timeout, 5s keepalive, conservative flow control, 300ms initial RTT)
- `PathMonitor`: EWMA-smoothed loss, RTT, jitter, bandwidth estimation
- Connection lifecycle: `create_endpoint()`, `connect()`, `accept()`
- Self-signed certificate generation for testing
**Tests**: 55+ tests across all 4 crates (codec roundtrip, FEC recovery at 30/50/70% loss, crypto encrypt/decrypt, handshake, anti-replay, transport serialization, path monitoring)
## Phase 3: Integration (Relay + Client)
**Scope**: Wire all layers together into working relay and client binaries.
### wzp-relay
- Room mode (SFU): `RoomManager` with named rooms, auto-create/auto-delete, per-participant forwarding
- Forward mode: two-pipeline architecture (upstream/downstream) with FEC re-encode and jitter buffering
- `RelayPipeline`: ingest -> FEC decode -> jitter buffer -> pop -> FEC re-encode -> send
- `SessionManager`: tracks active sessions, max session limit, idle expiration
- Relay-side handshake: `accept_handshake()` with signature verification and profile negotiation
- `RelayConfig`: configurable listen address, remote relay, max sessions, jitter parameters
- Periodic stats logging (upstream/downstream packet counts)
### wzp-client
- `CallEncoder`: PCM -> audio encode -> FEC block management -> source + repair MediaPackets
- `CallDecoder`: MediaPacket -> FEC decode -> jitter buffer -> audio decode -> PCM
- Client-side handshake: `perform_handshake()` with ephemeral key exchange and signature
- CLI modes: silence test, tone generation (440 Hz), file send, file record, echo test, live audio
- `AudioCapture`/`AudioPlayback` via cpal (behind `audio` feature flag), supporting both i16 and f32 sample formats
- Automated echo test with windowed analysis (loss, SNR, correlation, degradation detection)
- Benchmark suite: codec roundtrip (1000 frames), FEC recovery (100 blocks), crypto throughput (30000 packets), full pipeline (50 frames)
**Tests**: 25+ tests for pipeline creation, packet generation, FEC repair generation, session management
## Phase 4: Web Bridge, Rooms, PTT, TLS
**Scope**: Browser support and multi-party calling.
### wzp-web
- Axum-based HTTP/WebSocket server
- Browser audio capture via AudioWorklet (primary) with ScriptProcessorNode fallback
- Browser audio playback via AudioWorklet with scheduled BufferSource fallback
- Room-based routing: `/ws/<room-name>` WebSocket endpoint
- Room name passed as QUIC SNI to the relay
- Push-to-talk (PTT) support: button, mouse hold, spacebar
- Audio level meter in the UI
- TLS support via `--tls` flag with self-signed certificate generation
- Auto-reconnection on WebSocket disconnect
- Static file serving for the web UI
## Current Status
### What Works
- Full encode/decode pipeline: PCM -> Opus/Codec2 -> FEC -> MediaPacket -> FEC decode -> audio decode -> PCM
- Adaptive codec switching between Opus and Codec2 (including resampling)
- RaptorQ FEC recovery at various loss rates (tested up to 50% loss)
- ChaCha20-Poly1305 encryption with deterministic nonces
- X25519 key exchange with Ed25519 identity signatures
- QUIC transport with DATAGRAM frames for media and reliable streams for signaling
- Single relay echo mode (connectivity test)
- Multi-party room calls (SFU)
- Two-relay forwarding chain
- Web browser audio via WebSocket bridge
- File-based send/record for testing
- Live microphone/speaker mode (with `audio` feature)
- Push-to-talk in the web UI
- Automated echo quality test with windowed analysis
- Performance benchmarks
- Cross-compilation CI for amd64, arm64, armv7
### Known Issues
- **Jitter buffer drift**: During long echo tests, the jitter buffer depth can drift because there is no adaptive depth adjustment based on observed jitter. The buffer uses sequence-number ordering only, without timestamp-based playout scheduling.
- **Web audio drift**: The browser AudioWorklet playback buffer caps at 200ms, but clock drift between the WebSocket message arrival rate and the AudioContext output rate can cause occasional underruns or accumulation. The cap prevents unbounded growth but may cause glitches.
- **Adaptive loop integration (resolved)**: AdaptiveQualityController wired into both desktop and Android send/recv tasks. Relay-coordinated codec switching broadcasts QualityDirective — now handled by both engines (fixed 2026-04-13). 5-tier classification (Studio64k through Catastrophic) with asymmetric hysteresis.
- **Relay FEC pass-through**: In room mode, the relay forwards packets opaquely without FEC decode/re-encode. This means FEC protection is end-to-end only, not per-hop. In forward mode, the relay pipeline does perform FEC decode/re-encode.
- **No certificate verification**: The QUIC client config uses `SkipServerVerification` (accepts any certificate). This is intentional for testing but must be addressed for production deployments.
## Test Coverage
372+ tests across 7 crates (wzp-web has no Rust tests):
| Crate | Test Count |
|-------|------------|
| wzp-proto | ~84 |
| wzp-codec | ~69 |
| wzp-fec | ~21 |
| wzp-crypto | ~21 |
| wzp-transport | ~11 |
| wzp-relay | ~120 |
| wzp-client | ~57 |
| **Total** | **372+** |
Tests cover:
- Wire format roundtrip (header, quality report, full packet)
- Codec encode/decode for all 5 codec IDs
- Adaptive codec switching (Opus <-> Codec2)
- FEC recovery at 0%, 30%, 50% loss
- Concurrent FEC block decoding
- Full key exchange handshake (Alice/Bob derive same session key)
- Encrypt/decrypt roundtrip, wrong-key rejection, wrong-AAD rejection
- Anti-replay window: sequential, out-of-order, duplicate, wrapping
- Rekeying: interval trigger, key derivation, old key zeroization
- QUIC datagram serialization roundtrip
- Path quality EWMA smoothing
- Jitter buffer: ordering, reordering, missing packets, min depth, duplicates
- Session state machine: happy path, invalid transitions, connection loss
- Pipeline packet generation and FEC repair
- Benchmark correctness (codec, FEC, crypto, pipeline)
## Performance Benchmarks
Run with `wzp-bench --all`. Representative results (Apple M-series, single core):
### Codec Roundtrip (Opus 24kbps)
- 1000 frames of 440 Hz sine wave (20ms each, 48 kHz mono)
- Encode: ~20-40 us/frame average
- Decode: ~10-20 us/frame average
- Throughput: >10,000 frames/sec (200x real-time)
- Compression ratio: ~30x (960 i16 samples = 1920 bytes -> ~60 bytes encoded)
### FEC Recovery
- 100 blocks of 5 frames each
- At 20% loss: ~100% recovery rate
- At 30% loss with scaled FEC ratio: >95% recovery rate
### Crypto (ChaCha20-Poly1305)
- 30,000 packets (60/120/256 byte payloads)
- Throughput: >500,000 packets/sec
- Bandwidth: >50 MB/sec
- Average latency: <2 us per encrypt+decrypt cycle
### Full Pipeline (E2E)
- 50 frames through CallEncoder -> CallDecoder
- Average E2E latency: ~100-200 us/frame (codec + FEC, no network)
- Wire overhead ratio: ~0.05-0.10x of raw PCM (high compression from Opus)
## Deployment Status
- **Local testing**: All modes tested on localhost (single relay, room mode, forward mode, web bridge)
- **Hetzner VPS**: Build script (`scripts/build-linux.sh`) tested for provisioning, building, and downloading Linux binaries
- **CI**: Gitea workflow defined for amd64/arm64/armv7 builds
- **Production**: Not yet deployed to production networks
## Recent Changes (2026-04-13)
### P2P Adaptive Quality (#23, 2026-04-13)
- QualityReport::from_path_stats() — construct reports from local quinn stats
- CallEncoder.pending_quality_report — one-shot attachment to source packets
- Send tasks generate quality reports every 50 frames (~1s) from path stats
- Recv tasks self-observe from own QUIC stats for P2P adaptation
- Both relay and P2P calls now have full adaptive quality
### Protocol Analyzer (#13-17, 2026-04-13)
- New binary: wzp-analyzer (crates/wzp-client/src/analyzer.rs, ~900 lines)
- Passive observer: joins room, receives all media, never sends
- TUI mode (ratatui): per-participant table with loss%, jitter, codec, color-coded
- No-TUI mode: stats printed to stderr every 2s
- Binary capture format (.wzp) with microsecond timestamps
- Replay mode: offline analysis from capture files
- HTML report: self-contained with Chart.js loss/jitter timelines
- Encrypted decode: stub (needs session key + nonce context for SFU E2E)
### Codebase Refactoring (2026-04-13)
- DashMap relay concurrency: global Mutex → 64-shard DashMap
- Federation clone-before-send: eliminated last lock-during-I/O
- Engine deduplication: 3 shared helpers, eliminated 250 lines duplication
- 29 federation tests (was 0)
- Clap CLI parser for relay (replaced 154-line manual parser)
- Magic number constants, error handling helpers, safety docs
### 5-Tier Adaptive Quality Classification (#9)
- `Tier` enum extended from 3 to 6 levels: Studio64k > Studio48k > Studio32k > Good > Degraded > Catastrophic
- WiFi thresholds: loss < 1%/RTT < 30ms (Studio64k) through loss >= 15%/RTT >= 200ms (Catastrophic)
- Cellular stays at Good ceiling (no studio tiers on mobile data)
- Asymmetric hysteresis: downgrade 3 reports, upgrade 5, studio upgrade 10
- `Tier` derives `Ord` — ordering matches quality level (Catastrophic=0, Studio64k=5)
- `weakest_tier()` simplified to `.min()` via Ord
### Client QualityDirective Handling (#27)
- Both desktop signal tasks (P2P and relay engines) now match `QualityDirective` signals
- Android signal task matches `QualityDirective` and stores profile index via `pending_profile_recv`
- Relay-coordinated codec switching now works end-to-end: relay broadcasts → clients react
- Closes the gap documented in PRD-coordinated-codec.md
### Debug Tap Enhancements (#11, #12)
- `log_signal()`: logs `RoomUpdate` (count + participant names), `QualityDirective` (codec + reason)
- `log_event()`: logs participant join/leave lifecycle events
- `log_stats()`: periodic 5-second summary — packets in/out, fan-out avg, seq gaps, codecs seen
- `TapStats` struct tracks per-participant metrics across the forwarding loop
- All output via `target: "debug_tap"` for RUST_LOG filtering
### Bug Fix: dual_path.rs Phase 7 regression
- Added missing `ipv6_endpoint: None` parameter to 3 `race()` call sites in integration tests
- Phase 7 IPv6 dual-socket changed the function signature but tests were not updated
### Build: Keystore sync (f17420a)
- `build.sh` syncs keystores from persistent cache before build
## Previous Changes (2026-04-12)
### Bluetooth Audio Routing
- 3-way route cycling: Earpiece → Speaker → Bluetooth SCO
- `setCommunicationDevice()` API 31+ with `startBluetoothSco()` fallback
- BT-mode Oboe: capture skips 48kHz + VoiceCommunication, Oboe resamples 8/16kHz ↔ 48kHz
- `MODE_IN_COMMUNICATION` deferred to call start (was at app launch — hijacked system audio)
### Network Change Detection
- `NetworkMonitor.kt` wraps `ConnectivityManager.NetworkCallback`
- WiFi/cellular classification via bandwidth heuristics (no READ_PHONE_STATE needed)
- Feeds `AdaptiveQualityController::signal_network_change()` via JNI → AtomicU8 → recv task
### Hangup Signal Fix
- `SignalMessage::Hangup` now carries optional `call_id`
- Relay only ends the named call (not all calls for the user)
- Fixes race: hangup for call 1 no longer kills newly-placed call 2
### Per-Architecture APK Builds
- `build-tauri-android.sh --arch arm64|armv7|all`
- Separate per-arch APKs (~25MB each vs ~50MB universal)
- Release APKs signed with `wzp-release.jks` via `apksigner`
### Continuous DRED Tuning (Phase A: opus-DRED-v2)
- `DredTuner` in `wzp-proto::dred_tuner` maps live network metrics to continuous DRED duration
- Polls quinn path stats every 25 frames (~500ms): loss%, RTT, jitter
- Linear interpolation between baseline and ceiling per codec tier (not discrete tier jumps)
- Jitter-spike detection: >30% EWMA spike pre-emptively boosts DRED to ceiling for ~5s
- RTT phantom loss: high RTT (>200ms) adds phantom contribution to keep DRED generous
- `set_expected_loss()` and `set_dred_duration()` added to `AudioEncoder` trait
- Integrated into both Android and desktop send tasks in engine.rs
### Extended DRED Window
- Opus6k DRED duration increased from 500ms to 1040ms (max libopus 1.5 supports)
- RDO-VAE naturally degrades quality at longer offsets — extra window costs ~1-2 kbps
### PMTUD (Path MTU Discovery)
- Quinn's PLPMTUD explicitly configured: initial 1200, upper bound 1452, 300s interval
- `QuinnPathSnapshot` exposes discovered MTU via `current_mtu` field
- `TrunkedForwarder` refreshes `max_bytes` from PMTUD (was hard-coded 1200)
- Federation trunk frames now fill the discovered path MTU automatically
### New Tests
- 4 DRED tuner integration tests in wzp-client (encoder adjustment, spike boost, Codec2 no-op, profile switch)
- 10 unit tests in wzp-proto for DredTuner mapping logic
- Jitter variance window tests in wzp-transport PathMonitor
- Pre-existing test fixes: added missing `build_version` fields to 7 SignalMessage constructors
### Desktop Adaptive Quality (#7, #31)
- `AdaptiveQualityController` wired into both Android and desktop send/recv tasks
- `pending_profile: Arc<AtomicU8>` bridge between recv (writer) and send (reader)
- Auto mode: ingests QualityReports from relay, switches encoder profile when adapter recommends
- `tx_codec` display string updated on profile switch for UI indicator
- `profile_to_index()` / `index_to_profile()` mapping for 6-tier range
### Relay Coordinated Codec Switching (#25, #26)
- `ParticipantQuality` struct in relay RoomManager tracks per-participant quality
- Quality reports from forwarded packets feed per-participant `AdaptiveQualityController`
- `weakest_tier()` computes room-wide worst tier across all participants
- `QualityDirective` SignalMessage variant: relay broadcasts recommended profile to all participants
- Triggered on tier change — instant, no negotiation (weakest-link policy)
### Oboe Stream State Polling (#35)
- C++ polling loop after `requestStart()`: checks `getState()` every 10ms for up to 2s
- Waits for both capture and playout streams to reach `Started` state
- Logs initial state, poll count, and final state for HAL debugging
- Does NOT fail on timeout — Rust-side stall detector remains as safety net
- Targets Nothing Phone A059 intermittent silent calls on cold start
### Opus6k Frame Starvation Fix (2026-04-13)
- Root cause: partial reads from capture ring consumed samples that were discarded on retry
- `audio_read_capture(&mut buf[..1920])` with only 960 available → read 960, loop retried from buf[0], overwriting
- Added `wzp_native_audio_capture_available()` — check before reading (matches desktop pattern)
- `frame_samples` made mutable and updated on adaptive profile switch
- `buf` sized to max frame (1920) with `[..frame_samples]` slices throughout
- Result: Opus6k frame rate restored from ~11/s to expected 25/s
### Build Script Fixes (2026-04-13)
- Stale APK cleanup: delete all APKs before build, prefer `*release*.apk` on upload
- APK signing: added zipalign + apksigner pipeline to `build.sh` (was in `build-tauri-android.sh` only)
- Keystore persistence: `$BASE_DIR/data/keystore/` cache synced into source tree before build
- Fixes: 384MB debug APK uploaded instead of 25MB release; unsigned APK on alt server
### Phase 8: Tailscale-Inspired STUN/ICE Enhancements (2026-04-14)
5 new modules in `wzp-client`, 83 new unit tests (588 total across workspace).
#### Public STUN Client (`stun.rs`)
- Minimal RFC 5389 STUN Binding Request/Response over raw UDP
- XOR-MAPPED-ADDRESS (preferred) + MAPPED-ADDRESS (fallback) parsing
- Default servers: `stun.l.google.com:19302`, `stun1.l.google.com:19302`, `stun.cloudflare.com:3478`
- `discover_reflexive()` — first-success parallel probe across N servers
- `probe_stun_servers()` — full results for NAT classification
- Integrated into `detect_nat_type_with_stun()` combining relay + STUN probes
- Desktop STUN fallback in `try_reflect_own_addr()` when relay reflection fails
#### PCP/PMP/UPnP Port Mapping (`portmap.rs`)
- **NAT-PMP** (RFC 6886): UDP to gateway:5351, external address + port mapping
- **PCP** (RFC 6887): PCP MAP opcode, IPv4-mapped IPv6 client address
- **UPnP IGD**: SSDP M-SEARCH discovery + SOAP `AddPortMapping`/`GetExternalIPAddress`
- Gateway discovery: macOS (`route -n get default`), Linux (`/proc/net/route`)
- `acquire_port_mapping()` tries NAT-PMP → PCP → UPnP, first success wins
- `release_port_mapping()` + `spawn_refresh()` for lifecycle management
- Signal protocol: `caller_mapped_addr`/`callee_mapped_addr` on offer/answer, `peer_mapped_addr` on CallSetup
- `PeerCandidates.mapped` — new candidate type in dial order (host → mapped → reflexive)
#### Mid-Call ICE Re-Gathering (`ice_agent.rs`)
- `IceAgent`: owns candidate lifecycle with `gather()`, `re_gather()`, `apply_peer_update()`
- Monotonic generation counter prevents stale candidate updates from reordering
- `SignalMessage::CandidateUpdate` — new signal for mid-call candidate exchange
- Relay forwards `CandidateUpdate` to call peer (same pattern as `MediaPathReport`)
- Desktop handles `CandidateUpdate` in signal recv loop, emits to JS frontend
- Transport hot-swap architecture designed (TODO: wire into live call engine)
#### Netcheck Diagnostic (`netcheck.rs`)
- `NetcheckReport`: NAT type, reflexive addr, IPv4/v6, port mapping, relay latencies, gateway
- `run_netcheck()` — parallel probes for STUN + relay + portmap + IPv6
- `format_report()` — human-readable diagnostic output
- CLI: `wzp-client --netcheck <relay>` runs diagnostic
#### Region-Based Relay Selection (`relay_map.rs`)
- `RelayMap` sorted by RTT, `preferred()` returns lowest-latency reachable relay
- `populate_from_ack()` — parses `RegisterPresenceAck.available_relays`
- Stale detection (`needs_reprobe()`, `stale_entries()`)
- `RegisterPresenceAck` extended with `relay_region` and `available_relays`
#### Hard NAT Port Allocation Detection (`stun.rs` Phase A)
- `PortAllocation` enum: `PortPreserving` / `Sequential { delta }` / `Random` / `Unknown`
- `detect_port_allocation()` — sequential STUN probes from single socket, analyzes external port sequence
- `classify_port_allocation()` — pure classifier with wraparound handling, jitter tolerance (±1), 60% threshold for noisy sequences
- `predict_ports(last_port, delta, offset, spread)` — generates target port range for sequential NATs
- `HardNatProbe` signal message for peer coordination (carries port_sequence, allocation, external_ip)
- Relay forwards `HardNatProbe` to call peer
- `NetcheckReport.port_allocation` field populated automatically
- 17 new tests for classification, prediction, serde, Display
#### Relay End-to-End Wiring (2026-04-14)
- `CallRegistry` stores + cross-wires `caller_mapped_addr`/`callee_mapped_addr` into `CallSetup.peer_mapped_addr`
- `RelayConfig` extended with `region` + `advertised_addr` fields
- `RegisterPresenceAck` populates `relay_region` from config, `available_relays` from federation peers
- Desktop `place_call`/`answer_call` call `acquire_port_mapping()` and fill mapped addr fields
- Legacy `build-android-docker.sh` renamed to `build-android-docker-LEGACY.sh` to prevent accidental use
## Wave 5: Video Infrastructure (2026-05-12)
**Tasks completed:** T5.1, T5.1.1, T5.2, T5.3, T5.4, T5.5, T5.6, T5.7, T5.7.1, T5.8
### Relay: Audio + Video Scoring
New files in `crates/wzp-relay/src/`:
- `audio_scorer.rs` — per-stream audio quality scorer tracking packet loss, codec consistency, bitrate stability
- `response_policy.rs` — relay response policy engine mapping scores to action thresholds
- `verdict.rs``Verdict` enum: `Allow`, `RateLimit`, `Drop`, `Malicious`
- `video_scorer.rs``VideoScorer` with legitimacy scoring: keyframe regularity, I/P ratio, bandwidth responsiveness. **Note: wired but `observe()` not yet called from room forwarding path — T6.2 follow-up open.**
### Video: H.265 + Quality Controller
New files in `crates/wzp-video/src/`:
- `controller.rs``VideoQualityController`: maps (bwe_bps, loss_pct, rtt_ms, priority_mode) to (target_bitrate, target_fps, target_resolution, simulcast_layer)
- `simulcast.rs` — simulcast layer management (base + enhancement layers)
- `encoder_mode.rs` — encoder mode selection (CBR/VBR, keyframe intervals, quality presets)
H.265 encode/decode path added to:
- `videotoolbox.rs` — VideoToolbox H.265 encoder + decoder (macOS/iOS)
- `mediacodec.rs` — MediaCodec H.265 encoder + decoder (Android; NDK 0.9 compile errors pending in T4.3.1.1)
**Test delta:** wzp-relay 99→127, wzp-video 43→71
---
## Wave 6: AV1 + Federation Gossip Design (2026-05-12)
**Tasks completed:** T6.1, T6.1.2, T6.2
### Video: AV1 Codec Support
New files in `crates/wzp-video/src/`:
- `av1_obu.rs` — AV1 OBU (Open Bitstream Unit) framing and depacketizer
- `dav1d.rs` — dav1d AV1 software decoder (non-Android; gated via cfg)
- `svt_av1.rs` — SVT-AV1 software encoder (non-Android; gated via cfg)
Updated files:
- `videotoolbox.rs` — VideoToolbox AV1 decoder + encoder (macOS M3+, iOS A17+)
- `mediacodec.rs` — MediaCodec AV1 (Android; compile errors pending)
- `factory.rs``create_video_encoder(codec, platform)` dispatcher added; H.264, H.265, AV1 wired
**T6.1.2 follow-up open:** `create_video_encoder(Av1Main, ...)` has no caller in the call engine yet — wiring step is unstarted.
### Relay: Federation Reputation Gossip (Design Phase)
- T6.3 design exploration committed at `1e729e4`
- `docs/PRD/PRD-relay-federation-gossip.md` — Ban-List Distribution approach selected (Approach 3)
- Implementation not started; task spec pending conversion
### Test Counts
**Test delta Wave 6:** wzp-video 76→88, wzp-relay 127→137
**Total workspace tests: 702** (excluding `wzp-android`)
| Crate | Tests |
|---|---|
| wzp-proto | 112 |
| wzp-codec | 69 |
| wzp-fec | 21 |
| wzp-crypto | 64 |
| wzp-transport | 11 |
| wzp-relay | 137 |
| wzp-client | 200 |
| wzp-video | 88 |
| wzp-web | 2 |
| wzp-native | 0 |
---
## Current Status (2026-05-25)
### What Works (Audio)
All audio path items from previous status section remain working. Additionally:
- MediaHeader v2 (16 bytes) deployed across all paths
- MiniHeader v2 (5 bytes with seq_delta) deployed
- Anti-replay windows per stream with media-type-aware sizing (audio 64, video 1024)
- Relay DashMap + RwLock concurrency model (T3.1 resolved the Mutex bottleneck)
### What Works (Video — partial)
- H.264 framer/depacketizer with FU-A fragmentation handling
- H.264, H.265, AV1 VideoToolbox encode/decode (macOS)
- AV1 dav1d + SVT-AV1 software path (non-Android)
- Video quality controller, simulcast, encoder mode selection (controller only; no active call wiring yet)
- Video scorer (scoring logic complete; not yet wired into relay forwarding)
- NACK framework (`nack.rs`; not yet wired into room forwarding)
### Open Blockers
- **Android video:** `mediacodec.rs` has 31 NDK 0.9 compile errors (T4.3.1.1 in progress)
- **AV1 call wiring:** `create_video_encoder(Av1Main, ...)` has no caller (T6.1.2 follow-up)
- **VideoScorer wiring:** `VideoScorer::observe()` commented out at `room.rs:1263` (T6.2 follow-up)
- **NACK wiring:** NACK path not wired into room forwarding (Phase V2/V4)
- **BWE:** `AdaptiveQualityController` does not consume `cwnd`/`bytes_in_flight` (Phase V2)
- **Crypto nonce bug:** `decrypt()` uses `recv_seq` instead of `MediaHeader.seq` (see AUDIT-2026-05-25.md C1)

View File

@@ -0,0 +1,163 @@
---
tags: [reference, wzp]
type: reference
---
# WZP Telemetry & Observability
## Overview
WarzonePhone exports Prometheus-compatible metrics from all services (relay, web bridge, client) for Grafana dashboards. Inter-relay health probes provide always-on monitoring with negligible bandwidth overhead via multiplexed test lines.
## Architecture
```
┌──────────┐ probe (1 pkt/s) ┌──────────┐
│ Relay A │◄─────────────────────►│ Relay B │
│ :4433 │ │ :4433 │
│ /metrics │ │ /metrics │
└────┬─────┘ └────┬─────┘
│ │
│ scrape │ scrape
▼ ▼
┌─────────────────────────────────────────────┐
│ Prometheus │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ Grafana │
│ ┌─────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ Relay │ │ Per-call │ │ Inter-relay │ │
│ │ Health │ │ Quality │ │ Latency Map │ │
│ └─────────┘ └──────────┘ └──────────────┘ │
└─────────────────────────────────────────────┘
```
## Metrics Exported
### Relay (`/metrics` on HTTP port, default :9090)
| 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 |
| `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 |
### Web Bridge (`/metrics` on same HTTP port)
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `wzp_web_active_connections` | Gauge | — | Current WebSocket connections |
| `wzp_web_frames_bridged_total` | Counter | `direction` (up/down) | Audio frames bridged |
| `wzp_web_auth_failures_total` | Counter | — | Browser auth failures |
| `wzp_web_handshake_latency_seconds` | Histogram | — | Relay handshake time |
### Inter-Relay Probes
| 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 |
### Client (JSONL file)
When `--metrics-file <path>` is used, the client writes one JSON object per second:
```json
{
"ts": "2026-03-28T06:30: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"
}
```
## Task Breakdown
### WZP-P2-T5: Telemetry & Observability
| ID | Task | Dependencies | Effort |
|----|------|-------------|--------|
| **S1** | Prometheus `/metrics` on relay | None | 2-3h |
| **S2** | Per-session metrics (jitter, loss, RTT) | S1 | 2-3h |
| **S3** | Prometheus `/metrics` on web bridge | None | 2h |
| **S4** | Client `--metrics-file` JSONL export | None | 2h |
| **S5** | Inter-relay health probe (`--probe`) | S1 | 4-6h |
| **S6** | Probe mesh mode (all relays probe each other) | S5 | 2-3h |
| **S7** | Grafana dashboard JSON | S1-S6 | 2h |
### Parallelization
- **Group A** (parallel): S1, S3, S4 — three different binaries, no file overlap
- **Group B** (sequential): S2 after S1, then S5 → S6
- **Last**: S7 after all metrics are defined
## Inter-Relay Health Probes
The probe is a multiplexed test line: one QUIC connection per peer relay, one silent media packet per second (~50 bytes/s). This provides:
- **Continuous RTT measurement**: Ping/Pong signals timed to <1ms precision
- **Loss detection**: Sequence gaps tracked over sliding 60s window
- **Jitter monitoring**: Variation in inter-packet arrival times
- **Outage detection**: `wzp_probe_up` drops to 0 within seconds
### Why multiplexed?
WZP already multiplexes media on a single QUIC connection. The probe session shares the same connection pool — no extra ports, no extra TLS handshakes. At 1 pkt/s of silence (~50 bytes after Opus encoding + headers), the overhead is negligible even on metered links.
### Probe mesh example
With 3 relays (A, B, C), each probes the other 2:
```
A → B: rtt=12ms loss=0.0% jitter=2ms
A → C: rtt=45ms loss=0.1% jitter=5ms
B → A: rtt=13ms loss=0.0% jitter=2ms
B → C: rtt=38ms loss=0.0% jitter=4ms
C → A: rtt=44ms loss=0.2% jitter=6ms
C → B: rtt=37ms loss=0.0% jitter=3ms
```
This matrix feeds the Grafana latency heatmap and triggers alerts on degradation.
## Usage
```bash
# Relay with metrics
wzp-relay --listen 0.0.0.0:4433 --metrics-port 9090
# Relay with metrics + probe peer
wzp-relay --listen 0.0.0.0:4433 --metrics-port 9090 --probe relay-b:4433
# Web bridge with metrics
wzp-web --port 8080 --relay 127.0.0.1:4433 --metrics-port 9091
# Client with JSONL telemetry
wzp-client --live --metrics-file /tmp/call-metrics.jsonl relay:4433
```
## Grafana Dashboard
The pre-built dashboard (`docs/grafana-dashboard.json`) includes:
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

274
vault/Reference/Usage.md Normal file
View File

@@ -0,0 +1,274 @@
---
tags: [reference, wzp]
type: reference
---
# WarzonePhone Usage Guide
## Prerequisites
- **Rust** 1.85+ (2024 edition)
- **System libraries** (Linux): `cmake`, `pkg-config`, `libasound2-dev` (for audio feature)
- **System libraries** (macOS): Xcode command line tools (CoreAudio is included)
## Building from Source
### All Binaries (Headless)
```bash
cargo build --release --bin wzp-relay --bin wzp-client --bin wzp-bench --bin wzp-web
```
### Client with Live Audio Support
```bash
cargo build --release --bin wzp-client --features audio
```
### Run All Tests
```bash
cargo test --workspace --lib
```
### Building for Linux (Remote Build Script)
The project includes `scripts/build-linux.sh` which 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/
```
The build script produces:
- `wzp-relay` -- relay daemon
- `wzp-client` -- headless client
- `wzp-client-audio` -- client with mic/speaker support (needs libasound2)
- `wzp-web` -- web bridge server
- `wzp-bench` -- performance benchmarks
### CI Build
The `.gitea/workflows/build.yml` workflow builds release binaries for:
- Linux amd64
- Linux arm64 (cross-compiled)
- Linux armv7 (cross-compiled)
Triggered on version tags (`v*`) or manual dispatch.
---
## Binaries and CLI Flags
### wzp-relay
The relay daemon that forwards media between clients.
```
Usage: wzp-relay [--listen <addr>] [--remote <addr>]
Options:
--listen <addr> Listen address (default: 0.0.0.0:4433)
--remote <addr> Remote relay for forwarding (disables room mode)
```
**Room mode** (default): Clients join rooms by name. Packets are forwarded to all other participants in the same room (SFU model). Room name comes from QUIC SNI or defaults to "default".
**Forward mode** (`--remote`): All traffic is forwarded to a remote relay. Used for chaining relays across lossy/censored links.
### wzp-client
The CLI test client for sending and receiving audio.
```
Usage: wzp-client [options] [relay-addr]
Options:
--live Live mic/speaker mode (requires --features audio)
--send-tone <secs> Send a 440Hz test tone for N seconds
--send-file <file> Send a raw PCM file (48kHz mono s16le)
--record <file.raw> Record received audio to raw PCM file
--echo-test <secs> Run automated echo quality test
```
Default relay address: `127.0.0.1:4433`
### wzp-bench
Performance benchmark tool.
```
Usage: wzp-bench [OPTIONS]
Options:
--codec Run codec roundtrip benchmark (Opus 24kbps, 1000 frames)
--fec Run FEC recovery benchmark (100 blocks)
--crypto Run encryption benchmark (30000 packets)
--pipeline Run full pipeline benchmark (50 frames E2E)
--all Run all benchmarks (default if no flag given)
--loss <N> FEC loss percentage for --fec (default: 20)
```
### wzp-web
Web bridge server that connects browser audio via WebSocket to the relay.
```
Usage: wzp-web [--port 8080] [--relay 127.0.0.1:4433] [--tls]
Options:
--port <port> HTTP/WebSocket port (default: 8080)
--relay <addr> WZP relay address (default: 127.0.0.1:4433)
--tls Enable HTTPS (self-signed cert, required for mic on Android/remote)
```
Room URLs: `http://host:port/<room-name>` or `https://host:port/<room-name>` with `--tls`.
---
## Deployment Examples
### 1. Single Relay Echo Test
Start a relay, send a tone, and record the echo:
```bash
# Terminal 1: Start relay
wzp-relay --listen 0.0.0.0:4433
# Terminal 2: Send 10s of 440Hz tone and record the response
wzp-client --send-tone 10 --record echo.raw 127.0.0.1:4433
```
Play the recording:
```bash
ffplay -f s16le -ar 48000 -ac 1 echo.raw
```
### 2. Two-Party Call Through Relay
Two clients connected to the same relay default room:
```bash
# Terminal 1: Relay
wzp-relay
# Terminal 2: Client A — send tone
wzp-client --send-tone 30 127.0.0.1:4433
# Terminal 3: Client B — record
wzp-client --record call.raw 127.0.0.1:4433
```
### 3. Multi-Party Room Call
Multiple clients join the same named room. The relay QUIC SNI determines the room. With the web bridge, room names come from the URL path:
```bash
# Relay
wzp-relay
# Web bridge
wzp-web --port 8080 --relay 127.0.0.1:4433
# Browser clients open:
# http://localhost:8080/my-room
# All clients on /my-room hear each other.
```
### 4. Two-Relay Chain (Lossy Link)
Chain two relays for crossing a censored or lossy network boundary:
```bash
# Destination-side relay (receives from the forward relay)
wzp-relay --listen 0.0.0.0:4433
# Client-side relay (forwards to the destination relay)
wzp-relay --listen 0.0.0.0:5433 --remote <dest-relay-ip>:4433
# Client connects to the client-side relay
wzp-client --send-tone 10 127.0.0.1:5433
```
### 5. Web Browser Call with TLS
TLS is required for microphone access on non-localhost origins (Android, remote browsers):
```bash
# Relay
wzp-relay
# Web bridge with TLS (self-signed certificate)
wzp-web --port 8443 --relay 127.0.0.1:4433 --tls
# Open in browser (accept self-signed cert warning):
# https://your-server:8443/room-name
```
The web UI supports:
- Open mic (default) and push-to-talk modes
- PTT via on-screen button, mouse hold, or spacebar
- Audio level meter
- Auto-reconnection on disconnect
### 6. Automated Echo Quality Test
```bash
wzp-relay &
wzp-client --echo-test 30 127.0.0.1:4433
```
Produces a windowed analysis report showing loss percentage, SNR, correlation, and detects quality degradation trends over time.
### 7. Live Audio Call (requires `--features audio`)
```bash
wzp-relay &
# Terminal 2
wzp-client --live 127.0.0.1:4433
# Terminal 3
wzp-client --live 127.0.0.1:4433
```
Both clients capture from the default microphone and play received audio through the default speaker. Press Ctrl+C to stop.
---
## Audio File Format
All raw PCM files use:
- Sample rate: **48 kHz**
- Channels: **1** (mono)
- Sample format: **signed 16-bit little-endian** (s16le)
### ffmpeg 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 directly
ffplay -f s16le -ar 48000 -ac 1 file.raw
# or with the newer channel layout syntax:
ffplay -f s16le -ar 48000 -ch_layout mono file.raw
```
### Sending an Audio File
```bash
# Convert your audio 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
```

View File

@@ -0,0 +1,513 @@
---
tags: [reference, wzp]
type: reference
---
# 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.
## Direct 1:1 Calling (Desktop + Android)
In addition to room-mode group calls, you can place direct calls to a specific peer by fingerprint. Direct calls bypass room state entirely — the relay is used purely as a signaling gateway and for media relay. There is no need for the callee to join a room beforehand; they just need to be registered with the same signal hub.
### UI elements in the direct-call panel
- **Place call field** — paste a fingerprint (the long hex string you see under your own identity) and click Call. The callee sees a ringing UI.
- **Recent contacts row** — a horizontal strip of chips showing your most recently called/receiving peers. Click a chip to re-dial. Aliases are shown if the peer has one, otherwise a short fingerprint prefix.
- **Call history list** — every direct call you've placed, received, or missed, with direction indicator (↗ Outgoing, ↙ Incoming, ✗ Missed), the peer's alias (if known) or fingerprint prefix, and a timestamp. Click an entry to re-dial.
- **Deregister button** — drops your signal-hub registration without quitting the app. Useful when switching identities (e.g. testing with two accounts on one machine) or when you want to explicitly appear offline to peers.
- **Clear history button** — wipes the call history store. Does not affect current calls.
### Live updates
The call history updates in real time across all views via Tauri events (`history-changed`). Placing, answering, or missing a call immediately refreshes the history list and the recent contacts row — no manual refresh needed.
### Default room
On first launch, the room name in the room-mode panel defaults to `general` (changed from the prior `android` default so the desktop and Android clients don't silently talk past each other). You can still change it to any room name, and the last-used room is remembered across launches.
### Random alias
New installations derive a human-friendly alias from your identity seed — something like `silent-forest-41` or `bold-river-07`. It's deterministic, so reinstalling without changing your seed gives you the same alias. The alias is shown alongside your fingerprint in the header and is what peers see in their call history when they receive your call.
You can override the alias in Settings → Identity if you want a specific name.
## Windows AEC Variants
The Windows desktop build ships in two variants for echo cancellation, depending on which backend you want to exercise. Both are `wzp-desktop.exe` binaries — only the internal audio backend differs.
| Build | File | Capture backend | AEC | When to use |
|---|---|---|---|---|
| **noAEC baseline** | `wzp-desktop-noAEC.exe` | CPAL (WASAPI shared mode) | None | Headphone-only use, or for A/B comparison against the AEC build |
| **Communications AEC** | `wzp-desktop.exe` | Direct WASAPI with `AudioCategory_Communications` | **Yes** — Windows routes the capture stream through the driver's communications APO chain (AEC + noise suppression + automatic gain control) | Any speaker-mode call, laptop built-in speakers, anywhere echo is audible |
**Quality caveat**: the communications AEC operates at the OS level and its algorithm depends on the audio driver's installed APO chain. On modern consumer laptops with Intel Smart Sound, Dolby, recent Realtek, or Windows 11 Voice Clarity, the quality is excellent (effectively matching what Teams/Zoom deliver). On generic class-compliant USB microphones or older drivers, the communications APO may not be present at all — in that case the build behaves identically to the noAEC baseline.
If you hear echo on the AEC build, try these in order before escalating:
1. **Check which capture device is selected as "Default Device - Communications"** in Windows Sound Settings → Recording tab. Right-click any device to set it. The AEC build opens the device marked as `eCommunications`, not `eConsole`, so changing the default-communications device changes what we capture from.
2. **Verify the driver exposes a communications APO**. Sound Settings → Recording → your mic → Properties → Advanced → look for an "Enhancements" or "Signal Enhancements" tab. If it's absent, the driver has no APOs and the AEC build effectively has no AEC.
3. **Try the classic Voice Capture DSP build** when it ships (tracked as task #26). That uses Microsoft's bundled software AEC (`CLSID_CWMAudioAEC`) which works on every Windows machine regardless of driver.
### Installing the Windows builds
1. Windows 10: install the [WebView2 Runtime Evergreen Bootstrapper](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) first. Windows 11 has it pre-installed.
2. Copy `wzp-desktop.exe` (or `wzp-desktop-noAEC.exe`) to any directory and double-click. No installer needed.
3. First launch creates the config + identity store at `%APPDATA%\com.wzp.phone\`.

View File

@@ -0,0 +1,235 @@
---
tags: [reference, wzp]
type: reference
---
# Shared Crate Strategy: WZP ↔ featherChat
**Goal:** Both projects import each other's crates directly instead of duplicating code. A change to identity derivation in featherChat automatically applies in WZP, and vice versa for call signaling types.
---
## Current Problem
- `warzone-protocol` uses workspace dependency inheritance (`Cargo.toml` has `ed25519-dalek.workspace = true`). When WZP tries to use it as a path dep, Cargo fails because it can't resolve workspace references from outside the featherChat workspace.
- WZP had to mirror featherChat's `identity.rs`, `mnemonic.rs`, and `Fingerprint` type in `wzp-crypto/src/identity.rs` — duplicate code that can drift.
- featherChat will need `wzp_proto::SignalMessage` for the `WireMessage::CallSignal` variant — another potential duplication.
## Solution: Make Key Crates Standalone-Importable
### What featherChat Needs to Do
#### FC-CRATE-1: Make `warzone-protocol` standalone-publishable
**File:** `warzone/crates/warzone-protocol/Cargo.toml`
Replace all `workspace = true` references with explicit versions:
```toml
# Before:
ed25519-dalek.workspace = true
x25519-dalek.workspace = true
# After:
ed25519-dalek = { version = "2", features = ["serde", "rand_core"] }
x25519-dalek = { version = "2", features = ["serde", "static_secrets"] }
chacha20poly1305 = "0.10"
hkdf = "0.12"
sha2 = "0.10"
rand = "0.8"
bip39 = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
bincode = "1"
thiserror = "2"
hex = "0.4"
base64 = "0.22"
uuid = { version = "1", features = ["v4"] }
zeroize = { version = "1", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }
k256 = { version = "0.13", features = ["ecdsa", "serde"] }
tiny-keccak = { version = "2", features = ["keccak"] }
```
**Keep workspace inheritance working too** by using the `[package]` fallback pattern:
```toml
[package]
name = "warzone-protocol"
version = "0.0.20"
edition = "2021"
# Remove version.workspace and edition.workspace — use explicit values
```
This way the crate still works inside the featherChat workspace AND can be imported by WZP as a path dependency.
**Test:** From the WZP repo, this should work:
```toml
# In wzp-crypto/Cargo.toml:
warzone-protocol = { path = "../../deps/featherchat/warzone/crates/warzone-protocol" }
```
**Effort:** 30 minutes. Mechanical replacement, then `cargo build` to verify.
#### FC-CRATE-2: Add `wzp-proto` as a git dependency for `CallSignal`
**File:** `warzone/crates/warzone-protocol/Cargo.toml`
```toml
[dependencies]
# WarzonePhone signaling types (for CallSignal WireMessage variant)
wzp-proto = { git = "ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git", optional = true }
[features]
default = []
wzp = ["wzp-proto"]
```
**File:** `warzone/crates/warzone-protocol/src/message.rs`
```rust
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum WireMessage {
// ... existing variants ...
/// Voice/video call signaling (requires "wzp" feature).
#[cfg(feature = "wzp")]
CallSignal {
id: String,
sender_fingerprint: String,
signal: wzp_proto::SignalMessage, // Typed, not opaque bytes
},
/// Voice/video call signaling (without wzp feature — opaque bytes).
#[cfg(not(feature = "wzp"))]
CallSignal {
id: String,
sender_fingerprint: String,
signal: Vec<u8>, // Opaque JSON bytes
},
}
```
**Alternative (simpler):** Always use `Vec<u8>` for the signal field and let the consumer deserialize. This avoids the feature flag complexity:
```rust
CallSignal {
id: String,
sender_fingerprint: String,
signal_json: String, // JSON-serialized wzp_proto::SignalMessage
},
```
featherChat server treats it as opaque. WZP client deserializes it to `SignalMessage`.
**Effort:** 1-2 hours.
#### FC-CRATE-3: Extract shared identity types to a micro-crate (optional, long-term)
Create `warzone-identity` crate containing only:
- `Seed` (generation, from_bytes, from_hex, from_mnemonic, to_mnemonic)
- `IdentityKeyPair` (derive from seed)
- `PublicIdentity` (verifying key, encryption key, fingerprint)
- `Fingerprint` (SHA-256 truncated, display format)
- `hkdf_derive()` helper
Both `warzone-protocol` and `wzp-crypto` depend on `warzone-identity` instead of each implementing their own. This is the cleanest long-term solution but requires more refactoring.
**Crate structure:**
```
warzone-identity/
├── Cargo.toml (standalone, no workspace inheritance)
├── src/
│ ├── lib.rs
│ ├── seed.rs
│ ├── identity.rs
│ ├── fingerprint.rs
│ └── mnemonic.rs
```
**Dependencies:** ed25519-dalek, x25519-dalek, hkdf, sha2, bip39, hex, zeroize
Both projects import it:
```toml
# featherChat:
warzone-identity = { path = "../warzone-identity" }
# WZP (via submodule):
warzone-identity = { path = "deps/featherchat/warzone-identity" }
```
**Effort:** Half a day. Extract code from warzone-protocol, update imports in both projects.
---
### What WZP Needs to Do (after featherChat completes FC-CRATE-1)
#### WZP-CRATE-1: Replace identity mirror with real dependency
Once `warzone-protocol` is standalone-importable:
**File:** `crates/wzp-crypto/Cargo.toml`
```toml
# Remove bip39 and hex (now comes from warzone-protocol)
# Add:
warzone-protocol = { path = "../../deps/featherchat/warzone/crates/warzone-protocol" }
```
**File:** `crates/wzp-crypto/src/identity.rs`
Replace the entire file with re-exports:
```rust
//! featherChat identity — re-exported from warzone-protocol.
pub use warzone_protocol::identity::{IdentityKeyPair, Seed};
pub use warzone_protocol::types::Fingerprint;
```
**File:** `crates/wzp-crypto/src/handshake.rs`
Use `warzone_protocol::identity::Seed` internally instead of raw HKDF calls.
**Effort:** 1 hour (after FC-CRATE-1 is done).
#### WZP-CRATE-2: Make `wzp-proto` standalone-importable
`wzp-proto` already has explicit dependency versions (not workspace-inherited for external deps). It should work as a git dependency from featherChat. Verify:
```bash
# From a scratch project:
cargo add --git ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git wzp-proto
```
If this fails, replace any remaining workspace references in `wzp-proto/Cargo.toml` with explicit versions.
**Key types featherChat needs from wzp-proto:**
- `SignalMessage` (CallOffer, CallAnswer, IceCandidate, Hangup, etc.)
- `QualityProfile` (for codec negotiation)
- `HangupReason`
**Effort:** 30 minutes to verify and fix.
---
## Recommended Order
1. **FC-CRATE-1** — Make warzone-protocol standalone (30 min, unblocks everything)
2. **WZP-CRATE-2** — Verify wzp-proto works as git dep (30 min)
3. **FC-CRATE-2** — Add CallSignal with opaque signal_json field (1-2 hours)
4. **WZP-CRATE-1** — Replace identity mirror with real dep (1 hour)
5. **FC-CRATE-3** — Extract warzone-identity micro-crate (optional, half day)
After steps 1-4, both projects share types directly:
- WZP imports `warzone-protocol` for identity/seed/fingerprint
- featherChat imports `wzp-proto` (via git) for `SignalMessage` types
- No duplicated code, no drift risk
---
## Dependency Graph After Integration
```
warzone-identity (shared micro-crate, optional step 5)
↑ ↑
warzone-protocol wzp-crypto
↑ ↑
warzone-server wzp-proto ← wzp-codec, wzp-fec, wzp-transport
↑ ↑
warzone-client wzp-client, wzp-relay, wzp-web
```