T3.3: SignalMessage version field

This commit is contained in:
Siavash Sameni
2026-05-12 06:08:31 +04:00
parent 1b4f7b0772
commit e73f8a7150
30 changed files with 531 additions and 69 deletions

View File

@@ -23,7 +23,7 @@ use wzp_fec::{RaptorQFecDecoder, RaptorQFecEncoder};
use wzp_proto::{ use wzp_proto::{
AdaptiveQualityController, AudioDecoder, AudioEncoder, CodecId, FecDecoder, FecEncoder, AdaptiveQualityController, AudioDecoder, AudioEncoder, CodecId, FecDecoder, FecEncoder,
MediaHeader, MediaPacket, MediaTransport, MediaType, QualityController, QualityProfile, MediaHeader, MediaPacket, MediaTransport, MediaType, QualityController, QualityProfile,
SignalMessage, SignalMessage, default_signal_version,
}; };
use crate::audio_ring::AudioRing; use crate::audio_ring::AudioRing;
@@ -321,11 +321,12 @@ impl WzpEngine {
// Auth if token provided // Auth if token provided
if let Some(ref tok) = token { if let Some(ref tok) = token {
let _ = transport.send_signal(&SignalMessage::AuthToken { token: tok.clone() }).await; let _ = transport.send_signal(&SignalMessage::AuthToken { version: default_signal_version(), token: tok.clone() }).await;
} }
// Register presence // Register presence
let _ = transport.send_signal(&SignalMessage::RegisterPresence { let _ = transport.send_signal(&SignalMessage::RegisterPresence {
version: default_signal_version(),
identity_pub, identity_pub,
signature: vec![], signature: vec![],
alias: alias.clone(), alias: alias.clone(),
@@ -350,7 +351,7 @@ impl WzpEngine {
break; break;
} }
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(SignalMessage::CallRinging { call_id })) => { Ok(Some(SignalMessage::CallRinging { call_id, ..})) => {
info!(call_id = %call_id, "signal: ringing"); info!(call_id = %call_id, "signal: ringing");
let mut stats = signal_state.stats.lock().unwrap(); let mut stats = signal_state.stats.lock().unwrap();
stats.state = crate::stats::CallState::Ringing; stats.state = crate::stats::CallState::Ringing;
@@ -522,6 +523,7 @@ async fn run_call(
let signature = kx.sign(&sign_data); let signature = kx.sign(&sign_data);
let offer = SignalMessage::CallOffer { let offer = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature, signature,
@@ -1223,6 +1225,7 @@ async fn run_call(
Ok(Some(SignalMessage::RoomUpdate { Ok(Some(SignalMessage::RoomUpdate {
count, count,
participants, participants,
..
})) => { })) => {
info!(count, "RoomUpdate received"); info!(count, "RoomUpdate received");
let members: Vec<crate::stats::RoomMember> = participants let members: Vec<crate::stats::RoomMember> = participants
@@ -1240,6 +1243,7 @@ async fn run_call(
Ok(Some(SignalMessage::QualityDirective { Ok(Some(SignalMessage::QualityDirective {
recommended_profile, recommended_profile,
reason, reason,
..
})) => { })) => {
let idx = profile_to_index(&recommended_profile); let idx = profile_to_index(&recommended_profile);
info!( info!(

View File

@@ -15,7 +15,7 @@ use std::time::{Duration, Instant};
use clap::Parser; use clap::Parser;
use tracing::info; use tracing::info;
use wzp_proto::{CodecId, MediaPacket, MediaTransport}; use wzp_proto::{CodecId, MediaPacket, MediaTransport, default_signal_version};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// CLI // CLI
@@ -919,6 +919,7 @@ async fn main() -> anyhow::Result<()> {
// Auth if token provided // Auth if token provided
if let Some(ref token) = args.token { if let Some(ref token) = args.token {
let auth = wzp_proto::SignalMessage::AuthToken { let auth = wzp_proto::SignalMessage::AuthToken {
version: default_signal_version(),
token: token.clone(), token: token.clone(),
}; };
transport.send_signal(&auth).await?; transport.send_signal(&auth).await?;

View File

@@ -17,7 +17,7 @@ use std::sync::Arc;
use tracing::{error, info}; use tracing::{error, info};
use wzp_client::call::{CallConfig, CallDecoder, CallEncoder}; use wzp_client::call::{CallConfig, CallDecoder, CallEncoder};
use wzp_proto::MediaTransport; use wzp_proto::{MediaTransport, default_signal_version};
const FRAME_SAMPLES: usize = 960; // 20ms @ 48kHz const FRAME_SAMPLES: usize = 960; // 20ms @ 48kHz
@@ -380,6 +380,7 @@ async fn main() -> anyhow::Result<()> {
// Send auth token if provided (relay with --auth-url expects this first) // Send auth token if provided (relay with --auth-url expects this first)
if let Some(ref token) = cli.token { if let Some(ref token) = cli.token {
let auth = wzp_proto::SignalMessage::AuthToken { let auth = wzp_proto::SignalMessage::AuthToken {
version: default_signal_version(),
token: token.clone(), token: token.clone(),
}; };
transport.send_signal(&auth).await?; transport.send_signal(&auth).await?;
@@ -473,6 +474,7 @@ async fn run_silence(transport: Arc<wzp_transport::QuinnTransport>) -> anyhow::R
info!(total_source, total_repair, total_bytes, "done — closing"); info!(total_source, total_repair, total_bytes, "done — closing");
let hangup = wzp_proto::SignalMessage::Hangup { let hangup = wzp_proto::SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}; };
@@ -632,6 +634,7 @@ async fn run_file_mode(
// Send Hangup signal so the relay knows we're done // Send Hangup signal so the relay knows we're done
let hangup = wzp_proto::SignalMessage::Hangup { let hangup = wzp_proto::SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}; };
@@ -769,7 +772,7 @@ async fn run_signal_mode(
token: Option<String>, token: Option<String>,
call_target: Option<String>, call_target: Option<String>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
let identity = seed.derive_identity(); let identity = seed.derive_identity();
let pub_id = identity.public_identity(); let pub_id = identity.public_identity();
@@ -792,13 +795,17 @@ async fn run_signal_mode(
// Auth if token provided // Auth if token provided
if let Some(ref tok) = token { if let Some(ref tok) = token {
transport transport
.send_signal(&SignalMessage::AuthToken { token: tok.clone() }) .send_signal(&SignalMessage::AuthToken {
version: default_signal_version(),
token: tok.clone(),
})
.await?; .await?;
} }
// Register presence (signature not verified in Phase 1) // Register presence (signature not verified in Phase 1)
transport transport
.send_signal(&SignalMessage::RegisterPresence { .send_signal(&SignalMessage::RegisterPresence {
version: default_signal_version(),
identity_pub, identity_pub,
signature: vec![], // Phase 1: not verified signature: vec![], // Phase 1: not verified
alias: None, alias: None,
@@ -835,6 +842,7 @@ async fn run_signal_mode(
transport transport
.send_signal(&SignalMessage::DirectCallOffer { .send_signal(&SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: fp.clone(), caller_fingerprint: fp.clone(),
caller_alias: None, caller_alias: None,
target_fingerprint: target.clone(), target_fingerprint: target.clone(),
@@ -861,7 +869,7 @@ async fn run_signal_mode(
loop { loop {
match signal_transport.recv_signal().await { match signal_transport.recv_signal().await {
Ok(Some(msg)) => match msg { Ok(Some(msg)) => match msg {
SignalMessage::CallRinging { call_id } => { SignalMessage::CallRinging { call_id, .. } => {
info!(call_id = %call_id, "ringing..."); info!(call_id = %call_id, "ringing...");
} }
SignalMessage::DirectCallOffer { SignalMessage::DirectCallOffer {
@@ -879,6 +887,7 @@ async fn run_signal_mode(
// Auto-accept for CLI testing // Auto-accept for CLI testing
let _ = signal_transport let _ = signal_transport
.send_signal(&SignalMessage::DirectCallAnswer { .send_signal(&SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id, call_id,
accept_mode: wzp_proto::CallAcceptMode::AcceptGeneric, accept_mode: wzp_proto::CallAcceptMode::AcceptGeneric,
identity_pub: Some(identity_pub), identity_pub: Some(identity_pub),
@@ -908,6 +917,7 @@ async fn run_signal_mode(
peer_direct_addr: _, peer_direct_addr: _,
peer_local_addrs: _, peer_local_addrs: _,
peer_mapped_addr: _, peer_mapped_addr: _,
..
} => { } => {
info!(call_id = %call_id, room = %room, relay = %setup_relay, "call setup — connecting to media room"); info!(call_id = %call_id, room = %room, relay = %setup_relay, "call setup — connecting to media room");
@@ -970,6 +980,7 @@ async fn run_signal_mode(
_ = tokio::signal::ctrl_c() => { _ = tokio::signal::ctrl_c() => {
info!("hanging up..."); info!("hanging up...");
let _ = signal_transport.send_signal(&SignalMessage::Hangup { let _ = signal_transport.send_signal(&SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}).await; }).await;

View File

@@ -11,7 +11,7 @@
//! 5. Connects QUIC to relay for media //! 5. Connects QUIC to relay for media
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use wzp_proto::packet::SignalMessage; use wzp_proto::packet::{SignalMessage, default_signal_version};
/// featherChat CallSignal types (mirrors warzone-protocol::message::CallSignalType). /// featherChat CallSignal types (mirrors warzone-protocol::message::CallSignalType).
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@@ -152,6 +152,7 @@ mod tests {
#[test] #[test]
fn payload_roundtrip() { fn payload_roundtrip() {
let signal = SignalMessage::CallOffer { let signal = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub: [1u8; 32], identity_pub: [1u8; 32],
ephemeral_pub: [2u8; 32], ephemeral_pub: [2u8; 32],
signature: vec![3u8; 64], signature: vec![3u8; 64],
@@ -172,6 +173,7 @@ mod tests {
#[test] #[test]
fn signal_type_mapping() { fn signal_type_mapping() {
let offer = SignalMessage::CallOffer { let offer = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub: [0; 32], identity_pub: [0; 32],
ephemeral_pub: [0; 32], ephemeral_pub: [0; 32],
signature: vec![], signature: vec![],
@@ -183,6 +185,7 @@ mod tests {
assert!(matches!(signal_to_call_type(&offer), CallSignalType::Offer)); assert!(matches!(signal_to_call_type(&offer), CallSignalType::Offer));
let hangup = SignalMessage::Hangup { let hangup = SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}; };
@@ -209,6 +212,7 @@ mod tests {
)); ));
let transfer = SignalMessage::Transfer { let transfer = SignalMessage::Transfer {
version: default_signal_version(),
target_fingerprint: "abc".to_string(), target_fingerprint: "abc".to_string(),
relay_addr: None, relay_addr: None,
}; };

View File

@@ -4,7 +4,9 @@
//! send `CallOffer` → recv `CallAnswer` → derive shared `CryptoSession`. //! send `CallOffer` → recv `CallAnswer` → derive shared `CryptoSession`.
use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange}; use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange};
use wzp_proto::{HangupReason, MediaTransport, QualityProfile, SignalMessage}; use wzp_proto::{
HangupReason, MediaTransport, QualityProfile, SignalMessage, default_signal_version,
};
/// Errors that can occur during the client-side cryptographic handshake. /// Errors that can occur during the client-side cryptographic handshake.
#[derive(Debug)] #[derive(Debug)]
@@ -78,6 +80,7 @@ pub async fn perform_handshake(
// 4. Send CallOffer // 4. Send CallOffer
let offer = SignalMessage::CallOffer { let offer = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature, signature,
@@ -112,6 +115,7 @@ pub async fn perform_handshake(
ephemeral_pub, ephemeral_pub,
signature, signature,
chosen_profile, chosen_profile,
..
} => (identity_pub, ephemeral_pub, signature, chosen_profile), } => (identity_pub, ephemeral_pub, signature, chosen_profile),
SignalMessage::Hangup { SignalMessage::Hangup {
reason: HangupReason::ProtocolVersionMismatch { server_supported }, reason: HangupReason::ProtocolVersionMismatch { server_supported },

View File

@@ -17,7 +17,7 @@ use std::net::SocketAddr;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration; use std::time::Duration;
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
use crate::dual_path::PeerCandidates; use crate::dual_path::PeerCandidates;
use crate::portmap; use crate::portmap;
@@ -133,6 +133,7 @@ impl IceAgent {
let candidates = self.gather().await; let candidates = self.gather().await;
let update = SignalMessage::CandidateUpdate { let update = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: self.call_id.clone(), call_id: self.call_id.clone(),
reflexive_addr: candidates.reflexive.map(|a| a.to_string()), reflexive_addr: candidates.reflexive.map(|a| a.to_string()),
local_addrs: candidates.local.iter().map(|a| a.to_string()).collect(), local_addrs: candidates.local.iter().map(|a| a.to_string()).collect(),
@@ -206,6 +207,7 @@ mod tests {
// First update (gen=1) should succeed. // First update (gen=1) should succeed.
let update1 = SignalMessage::CandidateUpdate { let update1 = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-call".into(), call_id: "test-call".into(),
reflexive_addr: Some("203.0.113.5:4433".into()), reflexive_addr: Some("203.0.113.5:4433".into()),
local_addrs: vec!["192.168.1.10:4433".into()], local_addrs: vec!["192.168.1.10:4433".into()],
@@ -223,6 +225,7 @@ mod tests {
// Same generation (gen=1) should be rejected. // Same generation (gen=1) should be rejected.
let update1b = SignalMessage::CandidateUpdate { let update1b = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-call".into(), call_id: "test-call".into(),
reflexive_addr: Some("198.51.100.9:4433".into()), reflexive_addr: Some("198.51.100.9:4433".into()),
local_addrs: vec![], local_addrs: vec![],
@@ -233,6 +236,7 @@ mod tests {
// Older generation (gen=0) should be rejected. // Older generation (gen=0) should be rejected.
let update0 = SignalMessage::CandidateUpdate { let update0 = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-call".into(), call_id: "test-call".into(),
reflexive_addr: Some("10.0.0.1:4433".into()), reflexive_addr: Some("10.0.0.1:4433".into()),
local_addrs: vec![], local_addrs: vec![],
@@ -243,6 +247,7 @@ mod tests {
// Newer generation (gen=2) should succeed. // Newer generation (gen=2) should succeed.
let update2 = SignalMessage::CandidateUpdate { let update2 = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-call".into(), call_id: "test-call".into(),
reflexive_addr: Some("198.51.100.9:5555".into()), reflexive_addr: Some("198.51.100.9:5555".into()),
local_addrs: vec![], local_addrs: vec![],
@@ -287,6 +292,7 @@ mod tests {
let agent = IceAgent::new("test-call".into(), IceAgentConfig::default()); let agent = IceAgent::new("test-call".into(), IceAgentConfig::default());
let update = SignalMessage::CandidateUpdate { let update = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-call".into(), call_id: "test-call".into(),
reflexive_addr: Some("203.0.113.5:4433".into()), reflexive_addr: Some("203.0.113.5:4433".into()),
local_addrs: vec!["192.168.1.10:4433".into(), "10.0.0.5:4433".into()], local_addrs: vec!["192.168.1.10:4433".into(), "10.0.0.5:4433".into()],
@@ -315,6 +321,7 @@ mod tests {
let agent = IceAgent::new("test".into(), IceAgentConfig::default()); let agent = IceAgent::new("test".into(), IceAgentConfig::default());
let update = SignalMessage::CandidateUpdate { let update = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test".into(), call_id: "test".into(),
reflexive_addr: None, reflexive_addr: None,
local_addrs: vec![], local_addrs: vec![],
@@ -333,6 +340,7 @@ mod tests {
let agent = IceAgent::new("test".into(), IceAgentConfig::default()); let agent = IceAgent::new("test".into(), IceAgentConfig::default());
let update = SignalMessage::CandidateUpdate { let update = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test".into(), call_id: "test".into(),
reflexive_addr: Some("not-an-addr".into()), reflexive_addr: Some("not-an-addr".into()),
local_addrs: vec![ local_addrs: vec![

View File

@@ -30,7 +30,7 @@ use std::net::SocketAddr;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use serde::Serialize; use serde::Serialize;
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_transport::{QuinnTransport, client_config, create_endpoint}; use wzp_transport::{QuinnTransport, client_config, create_endpoint};
/// Result of one probe against one relay. Always returned so the /// Result of one probe against one relay. Always returned so the
@@ -123,6 +123,7 @@ pub async fn probe_reflect_addr(
// path does in desktop/src-tauri/src/lib.rs register_signal. // path does in desktop/src-tauri/src/lib.rs register_signal.
transport transport
.send_signal(&SignalMessage::RegisterPresence { .send_signal(&SignalMessage::RegisterPresence {
version: default_signal_version(),
identity_pub: [0u8; 32], identity_pub: [0u8; 32],
signature: vec![], signature: vec![],
alias: None, alias: None,
@@ -150,7 +151,7 @@ pub async fn probe_reflect_addr(
.map_err(|e| format!("send Reflect: {e}"))?; .map_err(|e| format!("send Reflect: {e}"))?;
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(SignalMessage::ReflectResponse { observed_addr })) => { Ok(Some(SignalMessage::ReflectResponse { observed_addr, .. })) => {
let parsed: SocketAddr = observed_addr let parsed: SocketAddr = observed_addr
.parse() .parse()
.map_err(|e| format!("parse observed_addr {observed_addr:?}: {e}"))?; .map_err(|e| format!("parse observed_addr {observed_addr:?}: {e}"))?;

View File

@@ -11,7 +11,7 @@ use tokio::sync::mpsc;
use wzp_proto::packet::MediaPacket; use wzp_proto::packet::MediaPacket;
use wzp_proto::traits::{MediaTransport, PathQuality}; use wzp_proto::traits::{MediaTransport, PathQuality};
use wzp_proto::{SignalMessage, TransportError}; use wzp_proto::{SignalMessage, TransportError, default_signal_version};
/// A mock transport backed by two mpsc channels (one per direction). /// A mock transport backed by two mpsc channels (one per direction).
/// ///
@@ -151,6 +151,7 @@ async fn handshake_rejects_tampered_signature() {
let bad_signature = kx.sign(b"wrong-data-intentionally"); let bad_signature = kx.sign(b"wrong-data-intentionally");
let offer = SignalMessage::CallOffer { let offer = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature: bad_signature, signature: bad_signature,
@@ -197,6 +198,7 @@ async fn client_receives_protocol_version_mismatch() {
// Respond with ProtocolVersionMismatch. // Respond with ProtocolVersionMismatch.
let mismatch = SignalMessage::Hangup { let mismatch = SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::ProtocolVersionMismatch { reason: wzp_proto::HangupReason::ProtocolVersionMismatch {
server_supported: vec![3], server_supported: vec![3],
}, },

View File

@@ -6,7 +6,7 @@
//! 3. Auth: WZP auth module request/response matches FC's /v1/auth/validate contract //! 3. Auth: WZP auth module request/response matches FC's /v1/auth/validate contract
//! 4. Mnemonic: BIP39 interop between both implementations //! 4. Mnemonic: BIP39 interop between both implementations
use wzp_proto::KeyExchange; use wzp_proto::{KeyExchange, default_signal_version};
// ─── Identity Compatibility (WZP-FC-8) ────────────────────────────────────── // ─── Identity Compatibility (WZP-FC-8) ──────────────────────────────────────
@@ -114,6 +114,7 @@ fn mnemonic_strings_identical() {
fn wzp_signal_serializes_into_fc_callsignal_payload() { fn wzp_signal_serializes_into_fc_callsignal_payload() {
// WZP creates a CallOffer SignalMessage // WZP creates a CallOffer SignalMessage
let offer = wzp_proto::SignalMessage::CallOffer { let offer = wzp_proto::SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub: [1u8; 32], identity_pub: [1u8; 32],
ephemeral_pub: [2u8; 32], ephemeral_pub: [2u8; 32],
signature: vec![3u8; 64], signature: vec![3u8; 64],
@@ -180,6 +181,7 @@ fn wzp_signal_serializes_into_fc_callsignal_payload() {
#[test] #[test]
fn wzp_answer_round_trips_through_fc_callsignal() { fn wzp_answer_round_trips_through_fc_callsignal() {
let answer = wzp_proto::SignalMessage::CallAnswer { let answer = wzp_proto::SignalMessage::CallAnswer {
version: default_signal_version(),
identity_pub: [10u8; 32], identity_pub: [10u8; 32],
ephemeral_pub: [20u8; 32], ephemeral_pub: [20u8; 32],
signature: vec![30u8; 64], signature: vec![30u8; 64],
@@ -212,6 +214,7 @@ fn wzp_answer_round_trips_through_fc_callsignal() {
#[test] #[test]
fn wzp_hangup_round_trips_through_fc_callsignal() { fn wzp_hangup_round_trips_through_fc_callsignal() {
let hangup = wzp_proto::SignalMessage::Hangup { let hangup = wzp_proto::SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}; };
@@ -298,6 +301,7 @@ fn all_signal_types_map_correctly() {
let cases: Vec<(wzp_proto::SignalMessage, &str)> = vec![ let cases: Vec<(wzp_proto::SignalMessage, &str)> = vec![
( (
wzp_proto::SignalMessage::CallOffer { wzp_proto::SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub: [0; 32], identity_pub: [0; 32],
ephemeral_pub: [0; 32], ephemeral_pub: [0; 32],
signature: vec![], signature: vec![],
@@ -310,6 +314,7 @@ fn all_signal_types_map_correctly() {
), ),
( (
wzp_proto::SignalMessage::CallAnswer { wzp_proto::SignalMessage::CallAnswer {
version: default_signal_version(),
identity_pub: [0; 32], identity_pub: [0; 32],
ephemeral_pub: [0; 32], ephemeral_pub: [0; 32],
signature: vec![], signature: vec![],
@@ -319,12 +324,14 @@ fn all_signal_types_map_correctly() {
), ),
( (
wzp_proto::SignalMessage::IceCandidate { wzp_proto::SignalMessage::IceCandidate {
version: default_signal_version(),
candidate: "candidate:1".to_string(), candidate: "candidate:1".to_string(),
}, },
"IceCandidate", "IceCandidate",
), ),
( (
wzp_proto::SignalMessage::Hangup { wzp_proto::SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}, },
@@ -482,10 +489,11 @@ fn auth_response_with_eth_address() {
assert!(!resp3.valid); assert!(!resp3.valid);
} }
/// WZP-S-7: SignalMessage::AuthToken { token } exists and round-trips via serde. /// WZP-S-7: SignalMessage::AuthToken { version: default_signal_version(), token } exists and round-trips via serde.
#[test] #[test]
fn wzp_proto_has_auth_token_variant() { fn wzp_proto_has_auth_token_variant() {
let msg = wzp_proto::SignalMessage::AuthToken { let msg = wzp_proto::SignalMessage::AuthToken {
version: default_signal_version(),
token: "fc-bearer-token-xyz".to_string(), token: "fc-bearer-token-xyz".to_string(),
}; };
@@ -496,7 +504,7 @@ fn wzp_proto_has_auth_token_variant() {
// Deserialize back // Deserialize back
let decoded: wzp_proto::SignalMessage = serde_json::from_str(&json).unwrap(); let decoded: wzp_proto::SignalMessage = serde_json::from_str(&json).unwrap();
if let wzp_proto::SignalMessage::AuthToken { token } = decoded { if let wzp_proto::SignalMessage::AuthToken { token, .. } = decoded {
assert_eq!(token, "fc-bearer-token-xyz"); assert_eq!(token, "fc-bearer-token-xyz");
} else { } else {
panic!("expected AuthToken variant, got: {decoded:?}"); panic!("expected AuthToken variant, got: {decoded:?}");

View File

@@ -32,7 +32,7 @@ pub use media_type::MediaType;
pub use packet::{ pub use packet::{
CallAcceptMode, FRAME_TYPE_FULL, FRAME_TYPE_MINI, HangupReason, MediaHeader, MediaHeaderV2, CallAcceptMode, FRAME_TYPE_FULL, FRAME_TYPE_MINI, HangupReason, MediaHeader, MediaHeaderV2,
MediaPacket, MiniFrameContext, MiniFrameContextV2, MiniHeader, MiniHeaderV2, PresenceUser, MediaPacket, MiniFrameContext, MiniFrameContextV2, MiniHeader, MiniHeaderV2, PresenceUser,
QualityReport, RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, QualityReport, RoomParticipant, SignalMessage, TrunkEntry, TrunkFrame, default_signal_version,
}; };
pub use quality::{AdaptiveQualityController, NetworkContext, Tier}; pub use quality::{AdaptiveQualityController, NetworkContext, Tier};
pub use session::{Session, SessionEvent, SessionState}; pub use session::{Session, SessionEvent, SessionState};

View File

@@ -540,10 +540,23 @@ impl MiniFrameContextV2 {
/// Compatible with Warzone messenger's identity model: /// Compatible with Warzone messenger's identity model:
/// - Identity keys are Ed25519 (signing) + X25519 (encryption) derived from a 32-byte seed via HKDF /// - Identity keys are Ed25519 (signing) + X25519 (encryption) derived from a 32-byte seed via HKDF
/// - Fingerprint = SHA-256(Ed25519 public key)[:16] /// - Fingerprint = SHA-256(Ed25519 public key)[:16]
///
/// **Version field:** every struct variant carries `version: u8` (default 1).
/// Old payloads that omit `version` deserialize cleanly thanks to `#[serde(default)]`.
///
/// **Unknown variant handling:** `#[serde(other)]` is designed for
/// string/integer enums with adjacent tagging, not for externally tagged enum
/// variants. With externally tagged representation (the default for Rust enums),
/// the variant name IS the tag, so there is no other value to catch. `bincode`
/// in particular does not support `#[serde(other)]`. Unknown variants will
/// naturally cause a deserialization error, which is the correct behavior for
/// the signal protocol.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SignalMessage { pub enum SignalMessage {
/// Call initiation (analogous to Warzone's WireMessage::CallOffer). /// Call initiation (analogous to Warzone's WireMessage::CallOffer).
CallOffer { CallOffer {
#[serde(default = "default_signal_version")]
version: u8,
/// Caller's Ed25519 identity public key (32 bytes). /// Caller's Ed25519 identity public key (32 bytes).
identity_pub: [u8; 32], identity_pub: [u8; 32],
/// Ephemeral X25519 public key for this call. /// Ephemeral X25519 public key for this call.
@@ -565,6 +578,8 @@ pub enum SignalMessage {
/// Call acceptance (analogous to Warzone's WireMessage::CallAnswer). /// Call acceptance (analogous to Warzone's WireMessage::CallAnswer).
CallAnswer { CallAnswer {
#[serde(default = "default_signal_version")]
version: u8,
/// Callee's Ed25519 identity public key (32 bytes). /// Callee's Ed25519 identity public key (32 bytes).
identity_pub: [u8; 32], identity_pub: [u8; 32],
/// Callee's ephemeral X25519 public key. /// Callee's ephemeral X25519 public key.
@@ -577,11 +592,15 @@ pub enum SignalMessage {
/// ICE candidate for NAT traversal. /// ICE candidate for NAT traversal.
IceCandidate { IceCandidate {
#[serde(default = "default_signal_version")]
version: u8,
candidate: String, candidate: String,
}, },
/// Periodic rekeying (forward secrecy). /// Periodic rekeying (forward secrecy).
Rekey { Rekey {
#[serde(default = "default_signal_version")]
version: u8,
/// New ephemeral X25519 public key. /// New ephemeral X25519 public key.
new_ephemeral_pub: [u8; 32], new_ephemeral_pub: [u8; 32],
/// Ed25519 signature over (new_ephemeral_pub || session_id). /// Ed25519 signature over (new_ephemeral_pub || session_id).
@@ -590,6 +609,8 @@ pub enum SignalMessage {
/// Quality/profile change request. /// Quality/profile change request.
QualityUpdate { QualityUpdate {
#[serde(default = "default_signal_version")]
version: u8,
report: QualityReport, report: QualityReport,
recommended_profile: crate::QualityProfile, recommended_profile: crate::QualityProfile,
}, },
@@ -601,6 +622,8 @@ pub enum SignalMessage {
/// introducing this variant is backward-compatible with pre-Phase-4 /// introducing this variant is backward-compatible with pre-Phase-4
/// relays — they'll just log "unknown signal variant" on receipt. /// relays — they'll just log "unknown signal variant" on receipt.
LossRecoveryUpdate { LossRecoveryUpdate {
#[serde(default = "default_signal_version")]
version: u8,
/// Total frames reconstructed via DRED since call start (monotonic). /// Total frames reconstructed via DRED since call start (monotonic).
#[serde(default)] #[serde(default)]
dred_reconstructions: u64, dred_reconstructions: u64,
@@ -616,9 +639,13 @@ pub enum SignalMessage {
/// Connection keepalive / RTT measurement. /// Connection keepalive / RTT measurement.
Ping { Ping {
#[serde(default = "default_signal_version")]
version: u8,
timestamp_ms: u64, timestamp_ms: u64,
}, },
Pong { Pong {
#[serde(default = "default_signal_version")]
version: u8,
timestamp_ms: u64, timestamp_ms: u64,
}, },
@@ -626,6 +653,8 @@ pub enum SignalMessage {
/// with older clients that send Hangup without it — the relay falls /// with older clients that send Hangup without it — the relay falls
/// back to ending ALL active calls for the sender in that case. /// back to ending ALL active calls for the sender in that case.
Hangup { Hangup {
#[serde(default = "default_signal_version")]
version: u8,
reason: HangupReason, reason: HangupReason,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
call_id: Option<String>, call_id: Option<String>,
@@ -634,6 +663,8 @@ pub enum SignalMessage {
/// featherChat bearer token for relay authentication. /// featherChat bearer token for relay authentication.
/// Sent as the first signal message when --auth-url is configured. /// Sent as the first signal message when --auth-url is configured.
AuthToken { AuthToken {
#[serde(default = "default_signal_version")]
version: u8,
token: String, token: String,
}, },
@@ -647,6 +678,8 @@ pub enum SignalMessage {
Unmute, Unmute,
/// Transfer the call to another peer. /// Transfer the call to another peer.
Transfer { Transfer {
#[serde(default = "default_signal_version")]
version: u8,
target_fingerprint: String, target_fingerprint: String,
/// Optional relay address for the transfer target. /// Optional relay address for the transfer target.
relay_addr: Option<String>, relay_addr: Option<String>,
@@ -658,6 +691,8 @@ pub enum SignalMessage {
/// Sent periodically over probe connections to share which fingerprints /// Sent periodically over probe connections to share which fingerprints
/// are connected to the sending relay. /// are connected to the sending relay.
PresenceUpdate { PresenceUpdate {
#[serde(default = "default_signal_version")]
version: u8,
/// Fingerprints currently connected to the sending relay. /// Fingerprints currently connected to the sending relay.
fingerprints: Vec<String>, fingerprints: Vec<String>,
/// Address of the sending relay (e.g., "192.168.1.10:4433"). /// Address of the sending relay (e.g., "192.168.1.10:4433").
@@ -666,11 +701,15 @@ pub enum SignalMessage {
/// Ask a peer relay to look up a fingerprint in its registry. /// Ask a peer relay to look up a fingerprint in its registry.
RouteQuery { RouteQuery {
#[serde(default = "default_signal_version")]
version: u8,
fingerprint: String, fingerprint: String,
ttl: u8, ttl: u8,
}, },
/// Response to a route query. /// Response to a route query.
RouteResponse { RouteResponse {
#[serde(default = "default_signal_version")]
version: u8,
fingerprint: String, fingerprint: String,
found: bool, found: bool,
relay_chain: Vec<String>, relay_chain: Vec<String>,
@@ -680,6 +719,8 @@ pub enum SignalMessage {
/// Sent over a relay link (`_relay` SNI) to ask the peer relay to /// Sent over a relay link (`_relay` SNI) to ask the peer relay to
/// create a room and forward media for the given session. /// create a room and forward media for the given session.
SessionForward { SessionForward {
#[serde(default = "default_signal_version")]
version: u8,
session_id: String, session_id: String,
target_fingerprint: String, target_fingerprint: String,
source_relay: String, source_relay: String,
@@ -687,12 +728,16 @@ pub enum SignalMessage {
/// Confirm that the forwarding session has been set up on the peer relay. /// Confirm that the forwarding session has been set up on the peer relay.
/// The `room_name` tells the source relay which room to address media to. /// The `room_name` tells the source relay which room to address media to.
SessionForwardAck { SessionForwardAck {
#[serde(default = "default_signal_version")]
version: u8,
session_id: String, session_id: String,
room_name: String, room_name: String,
}, },
/// Room membership update — sent by relay to all participants when someone joins or leaves. /// Room membership update — sent by relay to all participants when someone joins or leaves.
RoomUpdate { RoomUpdate {
#[serde(default = "default_signal_version")]
version: u8,
/// Current participant count. /// Current participant count.
count: u32, count: u32,
/// List of participants currently in the room. /// List of participants currently in the room.
@@ -702,12 +747,16 @@ pub enum SignalMessage {
// ── Federation signals (relay-to-relay) ── // ── Federation signals (relay-to-relay) ──
/// Federation: initial handshake — the connecting relay identifies itself. /// Federation: initial handshake — the connecting relay identifies itself.
FederationHello { FederationHello {
#[serde(default = "default_signal_version")]
version: u8,
/// TLS certificate fingerprint of the connecting relay. /// TLS certificate fingerprint of the connecting relay.
tls_fingerprint: String, tls_fingerprint: String,
}, },
/// Federation: this relay now has local participants in a global room. /// Federation: this relay now has local participants in a global room.
GlobalRoomActive { GlobalRoomActive {
#[serde(default = "default_signal_version")]
version: u8,
room: String, room: String,
/// Participants on the announcing relay (for federated presence). /// Participants on the announcing relay (for federated presence).
#[serde(default)] #[serde(default)]
@@ -716,6 +765,8 @@ pub enum SignalMessage {
/// Federation: this relay's last local participant left a global room. /// Federation: this relay's last local participant left a global room.
GlobalRoomInactive { GlobalRoomInactive {
#[serde(default = "default_signal_version")]
version: u8,
room: String, room: String,
}, },
@@ -723,6 +774,8 @@ pub enum SignalMessage {
/// Register on relay for direct calls. Sent on `_signal` connections /// Register on relay for direct calls. Sent on `_signal` connections
/// after optional AuthToken. /// after optional AuthToken.
RegisterPresence { RegisterPresence {
#[serde(default = "default_signal_version")]
version: u8,
/// Client's Ed25519 identity public key. /// Client's Ed25519 identity public key.
identity_pub: [u8; 32], identity_pub: [u8; 32],
/// Signature over ("register-presence" || identity_pub). /// Signature over ("register-presence" || identity_pub).
@@ -733,6 +786,8 @@ pub enum SignalMessage {
/// Relay confirms presence registration. /// Relay confirms presence registration.
RegisterPresenceAck { RegisterPresenceAck {
#[serde(default = "default_signal_version")]
version: u8,
success: bool, success: bool,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>, error: Option<String>,
@@ -750,6 +805,8 @@ pub enum SignalMessage {
/// Direct call offer routed through the relay to a specific peer. /// Direct call offer routed through the relay to a specific peer.
DirectCallOffer { DirectCallOffer {
#[serde(default = "default_signal_version")]
version: u8,
/// Caller's fingerprint. /// Caller's fingerprint.
caller_fingerprint: String, caller_fingerprint: String,
/// Caller's display name. /// Caller's display name.
@@ -798,6 +855,8 @@ pub enum SignalMessage {
/// Callee's response to a direct call. /// Callee's response to a direct call.
DirectCallAnswer { DirectCallAnswer {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// How the callee accepts (or rejects). /// How the callee accepts (or rejects).
accept_mode: CallAcceptMode, accept_mode: CallAcceptMode,
@@ -838,6 +897,8 @@ pub enum SignalMessage {
/// Relay tells both parties: media room is ready. /// Relay tells both parties: media room is ready.
CallSetup { CallSetup {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// Room name on the relay for the media session (e.g., "_call:a1b2c3d4"). /// Room name on the relay for the media session (e.g., "_call:a1b2c3d4").
room: String, room: String,
@@ -871,6 +932,8 @@ pub enum SignalMessage {
/// Ringing notification (relay → caller, callee received the offer). /// Ringing notification (relay → caller, callee received the offer).
CallRinging { CallRinging {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
}, },
@@ -893,6 +956,8 @@ pub enum SignalMessage {
/// for IPv4, "[::1]:p" for IPv6. Clients parse it with /// for IPv4, "[::1]:p" for IPv6. Clients parse it with
/// `SocketAddr::from_str`. /// `SocketAddr::from_str`.
ReflectResponse { ReflectResponse {
#[serde(default = "default_signal_version")]
version: u8,
observed_addr: String, observed_addr: String,
}, },
@@ -910,6 +975,8 @@ pub enum SignalMessage {
/// and the other picks Relay — they now agree on the path /// and the other picks Relay — they now agree on the path
/// before any media flows. /// before any media flows.
MediaPathReport { MediaPathReport {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// Did the direct QUIC connection (P2P dial or accept) /// Did the direct QUIC connection (P2P dial or accept)
/// complete successfully on this side? /// complete successfully on this side?
@@ -931,6 +998,8 @@ pub enum SignalMessage {
/// — peers ignore updates with a generation <= their last-seen /// — peers ignore updates with a generation <= their last-seen
/// generation to handle reordering. /// generation to handle reordering.
CandidateUpdate { CandidateUpdate {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// New server-reflexive address (STUN-discovered or relay-reflected). /// New server-reflexive address (STUN-discovered or relay-reflected).
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -951,6 +1020,8 @@ pub enum SignalMessage {
/// and recent port sequence so the peer can predict which port /// and recent port sequence so the peer can predict which port
/// to dial. /// to dial.
HardNatProbe { HardNatProbe {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// Last observed external ports (most recent first). /// Last observed external ports (most recent first).
/// Typically 3-5 entries from sequential STUN probes. /// Typically 3-5 entries from sequential STUN probes.
@@ -968,6 +1039,8 @@ pub enum SignalMessage {
/// ports it has open. The Dialer then sprays QUIC connects to /// ports it has open. The Dialer then sprays QUIC connects to
/// these ports (and optionally random ports) on the Acceptor's IP. /// these ports (and optionally random ports) on the Acceptor's IP.
HardNatBirthdayStart { HardNatBirthdayStart {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// Number of sockets the Acceptor opened. /// Number of sockets the Acceptor opened.
acceptor_port_count: u16, acceptor_port_count: u16,
@@ -995,6 +1068,8 @@ pub enum SignalMessage {
/// A→B→A echo loops; proper TTL + dedup will land when /// A→B→A echo loops; proper TTL + dedup will land when
/// multi-hop federation is added (Phase 4.2). /// multi-hop federation is added (Phase 4.2).
FederatedSignalForward { FederatedSignalForward {
#[serde(default = "default_signal_version")]
version: u8,
/// The signal message being forwarded /// The signal message being forwarded
/// (`DirectCallOffer`, `DirectCallAnswer`, `CallRinging`, /// (`DirectCallOffer`, `DirectCallAnswer`, `CallRinging`,
/// `Hangup`, ...). Boxed because `SignalMessage` is /// `Hangup`, ...). Boxed because `SignalMessage` is
@@ -1011,6 +1086,8 @@ pub enum SignalMessage {
/// Relay-initiated quality directive: all participants should switch /// Relay-initiated quality directive: all participants should switch
/// to the recommended profile to match the weakest link. /// to the recommended profile to match the weakest link.
QualityDirective { QualityDirective {
#[serde(default = "default_signal_version")]
version: u8,
recommended_profile: crate::QualityProfile, recommended_profile: crate::QualityProfile,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
reason: Option<String>, reason: Option<String>,
@@ -1021,6 +1098,8 @@ pub enum SignalMessage {
/// users to all connected clients. Sent on every register/ /// users to all connected clients. Sent on every register/
/// deregister so clients can maintain a live lobby user list. /// deregister so clients can maintain a live lobby user list.
PresenceList { PresenceList {
#[serde(default = "default_signal_version")]
version: u8,
/// List of online users. Each entry is { fingerprint, alias }. /// List of online users. Each entry is { fingerprint, alias }.
users: Vec<PresenceUser>, users: Vec<PresenceUser>,
}, },
@@ -1031,6 +1110,8 @@ pub enum SignalMessage {
/// conditions. Used for consensual upgrades that require both /// conditions. Used for consensual upgrades that require both
/// sides to agree (e.g., switching from Opus24k to Studio48k). /// sides to agree (e.g., switching from Opus24k to Studio48k).
UpgradeProposal { UpgradeProposal {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// Unique ID for this proposal (to match response). /// Unique ID for this proposal (to match response).
proposal_id: String, proposal_id: String,
@@ -1045,6 +1126,8 @@ pub enum SignalMessage {
/// Response to an UpgradeProposal. /// Response to an UpgradeProposal.
UpgradeResponse { UpgradeResponse {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
proposal_id: String, proposal_id: String,
/// true = accepted, both sides switch. false = rejected. /// true = accepted, both sides switch. false = rejected.
@@ -1057,6 +1140,8 @@ pub enum SignalMessage {
/// Confirmation that the upgrade is committed — both sides /// Confirmation that the upgrade is committed — both sides
/// should switch encoder at the next frame boundary. /// should switch encoder at the next frame boundary.
UpgradeConfirm { UpgradeConfirm {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
proposal_id: String, proposal_id: String,
confirmed_profile: crate::QualityProfile, confirmed_profile: crate::QualityProfile,
@@ -1067,6 +1152,8 @@ pub enum SignalMessage {
/// encoding where each side uses the best quality its connection /// encoding where each side uses the best quality its connection
/// supports, rather than forcing all to the weakest link. /// supports, rather than forcing all to the weakest link.
QualityCapability { QualityCapability {
#[serde(default = "default_signal_version")]
version: u8,
call_id: String, call_id: String,
/// The best profile this peer can sustain based on its /// The best profile this peer can sustain based on its
/// current network conditions. /// current network conditions.
@@ -1083,7 +1170,7 @@ pub enum SignalMessage {
/// carrying ACK/NACK vectors and a REMB-style bandwidth estimate. /// carrying ACK/NACK vectors and a REMB-style bandwidth estimate.
TransportFeedback { TransportFeedback {
/// Feedback format version (default 1). /// Feedback format version (default 1).
#[serde(default)] #[serde(default = "default_signal_version")]
version: u8, version: u8,
/// Which media stream this feedback applies to. /// Which media stream this feedback applies to.
stream_id: u8, stream_id: u8,
@@ -1132,6 +1219,10 @@ pub fn default_proto_version() -> u8 {
pub fn default_supported_versions() -> Vec<u8> { pub fn default_supported_versions() -> Vec<u8> {
vec![2] vec![2]
} }
/// Default signal message version (v1).
pub fn default_signal_version() -> u8 {
1
}
/// Reasons for ending a call. /// Reasons for ending a call.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -1304,12 +1395,13 @@ mod tests {
// for v6 and the client side has to parse that back. // for v6 and the client side has to parse that back.
for addr in ["192.0.2.17:4433", "[2001:db8::1]:4433", "127.0.0.1:54321"] { for addr in ["192.0.2.17:4433", "[2001:db8::1]:4433", "127.0.0.1:54321"] {
let resp = SignalMessage::ReflectResponse { let resp = SignalMessage::ReflectResponse {
version: default_signal_version(),
observed_addr: addr.to_string(), observed_addr: addr.to_string(),
}; };
let json = serde_json::to_string(&resp).unwrap(); let json = serde_json::to_string(&resp).unwrap();
let decoded: SignalMessage = serde_json::from_str(&json).unwrap(); let decoded: SignalMessage = serde_json::from_str(&json).unwrap();
match decoded { match decoded {
SignalMessage::ReflectResponse { observed_addr } => { SignalMessage::ReflectResponse { observed_addr, .. } => {
assert_eq!(observed_addr, addr); assert_eq!(observed_addr, addr);
// Must parse back to a SocketAddr cleanly. // Must parse back to a SocketAddr cleanly.
let _parsed: std::net::SocketAddr = observed_addr let _parsed: std::net::SocketAddr = observed_addr
@@ -1326,6 +1418,7 @@ mod tests {
// Wrap a DirectCallOffer inside FederatedSignalForward and // Wrap a DirectCallOffer inside FederatedSignalForward and
// prove both directions of serde preserve every field. // prove both directions of serde preserve every field.
let inner = SignalMessage::DirectCallOffer { let inner = SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: Some("Alice".into()), caller_alias: Some("Alice".into()),
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -1340,6 +1433,7 @@ mod tests {
caller_build_version: None, caller_build_version: None,
}; };
let forward = SignalMessage::FederatedSignalForward { let forward = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(inner), inner: Box::new(inner),
origin_relay_fp: "relay-a-tls-fp".into(), origin_relay_fp: "relay-a-tls-fp".into(),
}; };
@@ -1349,6 +1443,7 @@ mod tests {
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
inner, inner,
origin_relay_fp, origin_relay_fp,
..
} => { } => {
assert_eq!(origin_relay_fp, "relay-a-tls-fp"); assert_eq!(origin_relay_fp, "relay-a-tls-fp");
match *inner { match *inner {
@@ -1375,6 +1470,7 @@ mod tests {
// we intend to forward survives being boxed + re-serialized. // we intend to forward survives being boxed + re-serialized.
let cases: Vec<SignalMessage> = vec![ let cases: Vec<SignalMessage> = vec![
SignalMessage::DirectCallAnswer { SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
accept_mode: CallAcceptMode::AcceptTrusted, accept_mode: CallAcceptMode::AcceptTrusted,
identity_pub: None, identity_pub: None,
@@ -1387,9 +1483,11 @@ mod tests {
callee_build_version: None, callee_build_version: None,
}, },
SignalMessage::CallRinging { SignalMessage::CallRinging {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
}, },
SignalMessage::Hangup { SignalMessage::Hangup {
version: default_signal_version(),
reason: HangupReason::Normal, reason: HangupReason::Normal,
call_id: None, call_id: None,
}, },
@@ -1397,6 +1495,7 @@ mod tests {
for inner in cases { for inner in cases {
let inner_disc = std::mem::discriminant(&inner); let inner_disc = std::mem::discriminant(&inner);
let forward = SignalMessage::FederatedSignalForward { let forward = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(inner), inner: Box::new(inner),
origin_relay_fp: "r".into(), origin_relay_fp: "r".into(),
}; };
@@ -1415,6 +1514,7 @@ mod tests {
fn hole_punching_optional_fields_roundtrip() { fn hole_punching_optional_fields_roundtrip() {
// DirectCallOffer with Some(caller_reflexive_addr) // DirectCallOffer with Some(caller_reflexive_addr)
let offer = SignalMessage::DirectCallOffer { let offer = SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -1448,6 +1548,7 @@ mod tests {
// OMIT the field from the JSON so older relays that don't // OMIT the field from the JSON so older relays that don't
// know about caller_reflexive_addr don't see it. // know about caller_reflexive_addr don't see it.
let offer_none = SignalMessage::DirectCallOffer { let offer_none = SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -1469,6 +1570,7 @@ mod tests {
// DirectCallAnswer with callee_reflexive_addr. // DirectCallAnswer with callee_reflexive_addr.
let answer = SignalMessage::DirectCallAnswer { let answer = SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
accept_mode: CallAcceptMode::AcceptTrusted, accept_mode: CallAcceptMode::AcceptTrusted,
identity_pub: None, identity_pub: None,
@@ -1494,6 +1596,7 @@ mod tests {
// CallSetup with peer_direct_addr. // CallSetup with peer_direct_addr.
let setup = SignalMessage::CallSetup { let setup = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
room: "call-c1".into(), room: "call-c1".into(),
relay_addr: "203.0.113.5:4433".into(), relay_addr: "203.0.113.5:4433".into(),
@@ -1566,14 +1669,17 @@ mod tests {
// test a sample of the pre-existing ones. // test a sample of the pre-existing ones.
let cases = vec![ let cases = vec![
SignalMessage::Ping { SignalMessage::Ping {
version: default_signal_version(),
timestamp_ms: 12345, timestamp_ms: 12345,
}, },
SignalMessage::Hold, SignalMessage::Hold,
SignalMessage::Hangup { SignalMessage::Hangup {
version: default_signal_version(),
reason: HangupReason::Normal, reason: HangupReason::Normal,
call_id: None, call_id: None,
}, },
SignalMessage::CallRinging { SignalMessage::CallRinging {
version: default_signal_version(),
call_id: "abcd".into(), call_id: "abcd".into(),
}, },
]; ];
@@ -1614,6 +1720,7 @@ mod tests {
#[test] #[test]
fn transfer_serialize() { fn transfer_serialize() {
let transfer = SignalMessage::Transfer { let transfer = SignalMessage::Transfer {
version: default_signal_version(),
target_fingerprint: "abc123".to_string(), target_fingerprint: "abc123".to_string(),
relay_addr: Some("relay.example.com:4433".to_string()), relay_addr: Some("relay.example.com:4433".to_string()),
}; };
@@ -1623,6 +1730,7 @@ mod tests {
SignalMessage::Transfer { SignalMessage::Transfer {
target_fingerprint, target_fingerprint,
relay_addr, relay_addr,
..
} => { } => {
assert_eq!(target_fingerprint, "abc123"); assert_eq!(target_fingerprint, "abc123");
assert_eq!(relay_addr.unwrap(), "relay.example.com:4433"); assert_eq!(relay_addr.unwrap(), "relay.example.com:4433");
@@ -1632,6 +1740,7 @@ mod tests {
// Also test with relay_addr = None // Also test with relay_addr = None
let transfer_no_relay = SignalMessage::Transfer { let transfer_no_relay = SignalMessage::Transfer {
version: default_signal_version(),
target_fingerprint: "def456".to_string(), target_fingerprint: "def456".to_string(),
relay_addr: None, relay_addr: None,
}; };
@@ -1641,6 +1750,7 @@ mod tests {
SignalMessage::Transfer { SignalMessage::Transfer {
target_fingerprint, target_fingerprint,
relay_addr, relay_addr,
..
} => { } => {
assert_eq!(target_fingerprint, "def456"); assert_eq!(target_fingerprint, "def456");
assert!(relay_addr.is_none()); assert!(relay_addr.is_none());
@@ -1660,6 +1770,7 @@ mod tests {
#[test] #[test]
fn presence_update_signal_roundtrip() { fn presence_update_signal_roundtrip() {
let msg = SignalMessage::PresenceUpdate { let msg = SignalMessage::PresenceUpdate {
version: default_signal_version(),
fingerprints: vec!["aabb".to_string(), "ccdd".to_string()], fingerprints: vec!["aabb".to_string(), "ccdd".to_string()],
relay_addr: "10.0.0.1:4433".to_string(), relay_addr: "10.0.0.1:4433".to_string(),
}; };
@@ -1669,6 +1780,7 @@ mod tests {
SignalMessage::PresenceUpdate { SignalMessage::PresenceUpdate {
fingerprints, fingerprints,
relay_addr, relay_addr,
..
} => { } => {
assert_eq!(fingerprints.len(), 2); assert_eq!(fingerprints.len(), 2);
assert!(fingerprints.contains(&"aabb".to_string())); assert!(fingerprints.contains(&"aabb".to_string()));
@@ -1680,6 +1792,7 @@ mod tests {
// Empty fingerprints list // Empty fingerprints list
let msg_empty = SignalMessage::PresenceUpdate { let msg_empty = SignalMessage::PresenceUpdate {
version: default_signal_version(),
fingerprints: vec![], fingerprints: vec![],
relay_addr: "10.0.0.2:4433".to_string(), relay_addr: "10.0.0.2:4433".to_string(),
}; };
@@ -1689,6 +1802,7 @@ mod tests {
SignalMessage::PresenceUpdate { SignalMessage::PresenceUpdate {
fingerprints, fingerprints,
relay_addr, relay_addr,
..
} => { } => {
assert!(fingerprints.is_empty()); assert!(fingerprints.is_empty());
assert_eq!(relay_addr, "10.0.0.2:4433"); assert_eq!(relay_addr, "10.0.0.2:4433");
@@ -1999,6 +2113,7 @@ mod tests {
#[test] #[test]
fn quality_directive_roundtrip() { fn quality_directive_roundtrip() {
let msg = SignalMessage::QualityDirective { let msg = SignalMessage::QualityDirective {
version: default_signal_version(),
recommended_profile: crate::QualityProfile::DEGRADED, recommended_profile: crate::QualityProfile::DEGRADED,
reason: Some("weakest link degraded".into()), reason: Some("weakest link degraded".into()),
}; };
@@ -2008,6 +2123,7 @@ mod tests {
SignalMessage::QualityDirective { SignalMessage::QualityDirective {
recommended_profile, recommended_profile,
reason, reason,
..
} => { } => {
assert_eq!(recommended_profile.codec, CodecId::Opus6k); assert_eq!(recommended_profile.codec, CodecId::Opus6k);
assert_eq!(reason.as_deref(), Some("weakest link degraded")); assert_eq!(reason.as_deref(), Some("weakest link degraded"));
@@ -2019,6 +2135,7 @@ mod tests {
#[test] #[test]
fn quality_directive_without_reason_roundtrip() { fn quality_directive_without_reason_roundtrip() {
let msg = SignalMessage::QualityDirective { let msg = SignalMessage::QualityDirective {
version: default_signal_version(),
recommended_profile: crate::QualityProfile::GOOD, recommended_profile: crate::QualityProfile::GOOD,
reason: None, reason: None,
}; };
@@ -2078,6 +2195,7 @@ mod tests {
#[test] #[test]
fn upgrade_proposal_roundtrip() { fn upgrade_proposal_roundtrip() {
let msg = SignalMessage::UpgradeProposal { let msg = SignalMessage::UpgradeProposal {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
proposal_id: "p1".into(), proposal_id: "p1".into(),
proposed_profile: crate::QualityProfile::STUDIO_48K, proposed_profile: crate::QualityProfile::STUDIO_48K,
@@ -2102,6 +2220,7 @@ mod tests {
#[test] #[test]
fn upgrade_response_roundtrip() { fn upgrade_response_roundtrip() {
let msg = SignalMessage::UpgradeResponse { let msg = SignalMessage::UpgradeResponse {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
proposal_id: "p1".into(), proposal_id: "p1".into(),
accepted: true, accepted: true,
@@ -2118,6 +2237,7 @@ mod tests {
#[test] #[test]
fn upgrade_confirm_roundtrip() { fn upgrade_confirm_roundtrip() {
let msg = SignalMessage::UpgradeConfirm { let msg = SignalMessage::UpgradeConfirm {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
proposal_id: "p1".into(), proposal_id: "p1".into(),
confirmed_profile: crate::QualityProfile::STUDIO_64K, confirmed_profile: crate::QualityProfile::STUDIO_64K,
@@ -2137,6 +2257,7 @@ mod tests {
#[test] #[test]
fn quality_capability_roundtrip() { fn quality_capability_roundtrip() {
let msg = SignalMessage::QualityCapability { let msg = SignalMessage::QualityCapability {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
max_profile: crate::QualityProfile::GOOD, max_profile: crate::QualityProfile::GOOD,
loss_pct: Some(2.5), loss_pct: Some(2.5),
@@ -2162,6 +2283,7 @@ mod tests {
#[test] #[test]
fn candidate_update_roundtrip() { fn candidate_update_roundtrip() {
let msg = SignalMessage::CandidateUpdate { let msg = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "test-123".into(), call_id: "test-123".into(),
reflexive_addr: Some("203.0.113.5:4433".into()), reflexive_addr: Some("203.0.113.5:4433".into()),
local_addrs: vec!["192.168.1.10:4433".into(), "10.0.0.5:4433".into()], local_addrs: vec!["192.168.1.10:4433".into(), "10.0.0.5:4433".into()],
@@ -2177,6 +2299,7 @@ mod tests {
local_addrs, local_addrs,
mapped_addr, mapped_addr,
generation, generation,
..
} => { } => {
assert_eq!(call_id, "test-123"); assert_eq!(call_id, "test-123");
assert_eq!(reflexive_addr.as_deref(), Some("203.0.113.5:4433")); assert_eq!(reflexive_addr.as_deref(), Some("203.0.113.5:4433"));
@@ -2191,6 +2314,7 @@ mod tests {
#[test] #[test]
fn candidate_update_minimal_roundtrip() { fn candidate_update_minimal_roundtrip() {
let msg = SignalMessage::CandidateUpdate { let msg = SignalMessage::CandidateUpdate {
version: default_signal_version(),
call_id: "c".into(), call_id: "c".into(),
reflexive_addr: None, reflexive_addr: None,
local_addrs: vec![], local_addrs: vec![],
@@ -2215,6 +2339,7 @@ mod tests {
#[test] #[test]
fn offer_with_mapped_addr_roundtrip() { fn offer_with_mapped_addr_roundtrip() {
let msg = SignalMessage::DirectCallOffer { let msg = SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -2246,6 +2371,7 @@ mod tests {
#[test] #[test]
fn offer_without_mapped_addr_omits_field() { fn offer_without_mapped_addr_omits_field() {
let msg = SignalMessage::DirectCallOffer { let msg = SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -2266,6 +2392,7 @@ mod tests {
#[test] #[test]
fn answer_with_mapped_addr_roundtrip() { fn answer_with_mapped_addr_roundtrip() {
let msg = SignalMessage::DirectCallAnswer { let msg = SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
accept_mode: CallAcceptMode::AcceptTrusted, accept_mode: CallAcceptMode::AcceptTrusted,
identity_pub: None, identity_pub: None,
@@ -2292,6 +2419,7 @@ mod tests {
#[test] #[test]
fn setup_with_mapped_addr_roundtrip() { fn setup_with_mapped_addr_roundtrip() {
let msg = SignalMessage::CallSetup { let msg = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: "c1".into(), call_id: "c1".into(),
room: "room".into(), room: "room".into(),
relay_addr: "1.2.3.4:5".into(), relay_addr: "1.2.3.4:5".into(),
@@ -2368,6 +2496,7 @@ mod tests {
#[test] #[test]
fn register_presence_ack_with_new_fields_roundtrip() { fn register_presence_ack_with_new_fields_roundtrip() {
let msg = SignalMessage::RegisterPresenceAck { let msg = SignalMessage::RegisterPresenceAck {
version: default_signal_version(),
success: true, success: true,
error: None, error: None,
relay_build: Some("abc123".into()), relay_build: Some("abc123".into()),
@@ -2443,6 +2572,7 @@ mod tests {
nacked_seqs, nacked_seqs,
remb_bps, remb_bps,
recv_time_us, recv_time_us,
..
} => { } => {
assert_eq!(version, 1); assert_eq!(version, 1);
assert_eq!(stream_id, 0); assert_eq!(stream_id, 0);
@@ -2475,7 +2605,76 @@ mod tests {
let decoded: SignalMessage = serde_json::from_str(json).unwrap(); let decoded: SignalMessage = serde_json::from_str(json).unwrap();
match decoded { match decoded {
SignalMessage::TransportFeedback { version, .. } => { SignalMessage::TransportFeedback { version, .. } => {
assert_eq!(version, 0, "serde default makes omitted version 0"); assert_eq!(version, 1, "serde default makes omitted version 1");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn old_payload_without_version_deserializes() {
// CallOffer without version field — old client sending to new receiver.
let json = r#"{
"CallOffer": {
"identity_pub": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
"ephemeral_pub": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
"signature": [],
"supported_profiles": [],
"alias": null,
"protocol_version": 2,
"supported_versions": [2]
}
}"#;
let decoded: SignalMessage = serde_json::from_str(json).unwrap();
match decoded {
SignalMessage::CallOffer {
version,
protocol_version,
..
} => {
assert_eq!(version, 1, "missing version defaults to 1");
assert_eq!(protocol_version, 2);
}
_ => panic!("wrong variant"),
}
// Ping without version field.
let json = r#"{"Ping": {"timestamp_ms": 1234}}"#;
let decoded: SignalMessage = serde_json::from_str(json).unwrap();
match decoded {
SignalMessage::Ping {
version,
timestamp_ms,
} => {
assert_eq!(version, 1, "missing version defaults to 1");
assert_eq!(timestamp_ms, 1234);
}
_ => panic!("wrong variant"),
}
// Hangup without version field.
let json = r#"{"Hangup": {"reason": "Normal", "call_id": null}}"#;
let decoded: SignalMessage = serde_json::from_str(json).unwrap();
match decoded {
SignalMessage::Hangup { version, .. } => {
assert_eq!(version, 1, "missing version defaults to 1");
}
_ => panic!("wrong variant"),
}
}
#[test]
fn new_payload_with_version_deserializes() {
// Payload that explicitly includes version = 2.
let json = r#"{"Ping": {"version": 2, "timestamp_ms": 5678}}"#;
let decoded: SignalMessage = serde_json::from_str(json).unwrap();
match decoded {
SignalMessage::Ping {
version,
timestamp_ms,
} => {
assert_eq!(version, 2, "explicit version is preserved");
assert_eq!(timestamp_ms, 5678);
} }
_ => panic!("wrong variant"), _ => panic!("wrong variant"),
} }

View File

@@ -15,7 +15,7 @@ use sha2::{Digest, Sha256};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_transport::QuinnTransport; use wzp_transport::QuinnTransport;
use crate::config::{PeerConfig, TrustedConfig}; use crate::config::{PeerConfig, TrustedConfig};
@@ -520,7 +520,11 @@ async fn run_room_event_dispatcher(
if fm.is_global_room(&room) { if fm.is_global_room(&room) {
let participants = fm.room_mgr.local_participant_list(&room); let participants = fm.room_mgr.local_participant_list(&room);
info!(room = %room, count = participants.len(), "global room now active, announcing to peers"); info!(room = %room, count = participants.len(), "global room now active, announcing to peers");
let msg = SignalMessage::GlobalRoomActive { room, participants }; let msg = SignalMessage::GlobalRoomActive {
version: default_signal_version(),
room,
participants,
};
let transports: Vec<Arc<QuinnTransport>> = { let transports: Vec<Arc<QuinnTransport>> = {
let links = fm.peer_links.lock().await; let links = fm.peer_links.lock().await;
links.values().map(|l| l.transport.clone()).collect() links.values().map(|l| l.transport.clone()).collect()
@@ -533,7 +537,10 @@ async fn run_room_event_dispatcher(
Ok(RoomEvent::LocalLeave { room }) => { Ok(RoomEvent::LocalLeave { room }) => {
if fm.is_global_room(&room) { if fm.is_global_room(&room) {
info!(room = %room, "global room now inactive, announcing to peers"); info!(room = %room, "global room now inactive, announcing to peers");
let msg = SignalMessage::GlobalRoomInactive { room }; let msg = SignalMessage::GlobalRoomInactive {
version: default_signal_version(),
room,
};
let transports: Vec<Arc<QuinnTransport>> = { let transports: Vec<Arc<QuinnTransport>> = {
let links = fm.peer_links.lock().await; let links = fm.peer_links.lock().await;
links.values().map(|l| l.transport.clone()).collect() links.values().map(|l| l.transport.clone()).collect()
@@ -609,6 +616,7 @@ async fn run_stale_presence_sweeper(fm: Arc<FederationManager>) {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
all_participants.retain(|p| seen.insert(p.fingerprint.clone())); all_participants.retain(|p| seen.insert(p.fingerprint.clone()));
let update = SignalMessage::RoomUpdate { let update = SignalMessage::RoomUpdate {
version: default_signal_version(),
count: all_participants.len() as u32, count: all_participants.len() as u32,
participants: all_participants, participants: all_participants,
}; };
@@ -659,6 +667,7 @@ async fn connect_to_peer(
// Send hello with our TLS fingerprint // Send hello with our TLS fingerprint
let hello = SignalMessage::FederationHello { let hello = SignalMessage::FederationHello {
version: default_signal_version(),
tls_fingerprint: fm.local_tls_fp.clone(), tls_fingerprint: fm.local_tls_fp.clone(),
}; };
transport transport
@@ -710,6 +719,7 @@ async fn run_federation_link(
let participants = fm.room_mgr.local_participant_list(room_name); let participants = fm.room_mgr.local_participant_list(room_name);
info!(peer = %peer_label, room = %room_name, participants = participants.len(), "announcing local global room to new peer"); info!(peer = %peer_label, room = %room_name, participants = participants.len(), "announcing local global room to new peer");
msgs.push(SignalMessage::GlobalRoomActive { msgs.push(SignalMessage::GlobalRoomActive {
version: default_signal_version(),
room: room_name.clone(), room: room_name.clone(),
participants, participants,
}); });
@@ -724,6 +734,7 @@ async fn run_federation_link(
if fm.is_global_room(room) { if fm.is_global_room(room) {
info!(peer = %peer_label, room = %room, via = %link.label, "propagating remote room to new peer"); info!(peer = %peer_label, room = %room, via = %link.label, "propagating remote room to new peer");
msgs.push(SignalMessage::GlobalRoomActive { msgs.push(SignalMessage::GlobalRoomActive {
version: default_signal_version(),
room: room.clone(), room: room.clone(),
participants: participants.clone(), participants: participants.clone(),
}); });
@@ -837,7 +848,9 @@ async fn handle_signal(
} }
match msg { match msg {
SignalMessage::GlobalRoomActive { room, participants } => { SignalMessage::GlobalRoomActive {
room, participants, ..
} => {
if fm.is_global_room(&room) { if fm.is_global_room(&room) {
info!(peer = %peer_label, room = %room, remote_participants = participants.len(), "peer has global room active"); info!(peer = %peer_label, room = %room, remote_participants = participants.len(), "peer has global room active");
let mut links = fm.peer_links.lock().await; let mut links = fm.peer_links.lock().await;
@@ -882,6 +895,7 @@ async fn handle_signal(
let _ = link let _ = link
.transport .transport
.send_signal(&SignalMessage::GlobalRoomActive { .send_signal(&SignalMessage::GlobalRoomActive {
version: default_signal_version(),
room: room.clone(), room: room.clone(),
participants: tagged_for_propagation.clone(), participants: tagged_for_propagation.clone(),
}) })
@@ -923,6 +937,7 @@ async fn handle_signal(
let mut seen = HashSet::new(); let mut seen = HashSet::new();
all_participants.retain(|p| seen.insert(p.fingerprint.clone())); all_participants.retain(|p| seen.insert(p.fingerprint.clone()));
let update = SignalMessage::RoomUpdate { let update = SignalMessage::RoomUpdate {
version: default_signal_version(),
count: all_participants.len() as u32, count: all_participants.len() as u32,
participants: all_participants, participants: all_participants,
}; };
@@ -933,7 +948,7 @@ async fn handle_signal(
} }
} }
} }
SignalMessage::GlobalRoomInactive { room } => { SignalMessage::GlobalRoomInactive { room, .. } => {
info!(peer = %peer_label, room = %room, "peer global room now inactive"); info!(peer = %peer_label, room = %room, "peer global room now inactive");
let mut links = fm.peer_links.lock().await; let mut links = fm.peer_links.lock().await;
if let Some(link) = links.get_mut(peer_fp) { if let Some(link) = links.get_mut(peer_fp) {
@@ -999,6 +1014,7 @@ async fn handle_signal(
} }
} }
let msg = SignalMessage::GlobalRoomActive { let msg = SignalMessage::GlobalRoomActive {
version: default_signal_version(),
room: room.clone(), room: room.clone(),
participants: updated_participants, participants: updated_participants,
}; };
@@ -1007,7 +1023,10 @@ async fn handle_signal(
} }
} else { } else {
// No participants left anywhere — propagate inactive // No participants left anywhere — propagate inactive
let msg = SignalMessage::GlobalRoomInactive { room: room.clone() }; let msg = SignalMessage::GlobalRoomInactive {
version: default_signal_version(),
room: room.clone(),
};
for transport in &peer_sends { for transport in &peer_sends {
let _ = transport.send_signal(&msg).await; let _ = transport.send_signal(&msg).await;
} }
@@ -1025,6 +1044,7 @@ async fn handle_signal(
let mut seen = HashSet::new(); let mut seen = HashSet::new();
all_participants.retain(|p| seen.insert(p.fingerprint.clone())); all_participants.retain(|p| seen.insert(p.fingerprint.clone()));
let update = SignalMessage::RoomUpdate { let update = SignalMessage::RoomUpdate {
version: default_signal_version(),
count: all_participants.len() as u32, count: all_participants.len() as u32,
participants: all_participants, participants: all_participants,
}; };
@@ -1050,6 +1070,7 @@ async fn handle_signal(
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
inner, inner,
origin_relay_fp, origin_relay_fp,
..
} => { } => {
if origin_relay_fp == fm.local_tls_fp { if origin_relay_fp == fm.local_tls_fp {
tracing::debug!( tracing::debug!(

View File

@@ -4,7 +4,7 @@
//! recv `CallOffer` → verify → generate ephemeral → derive session → send `CallAnswer`. //! recv `CallOffer` → verify → generate ephemeral → derive session → send `CallAnswer`.
use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange}; use wzp_crypto::{CryptoSession, KeyExchange, WarzoneKeyExchange};
use wzp_proto::{MediaTransport, QualityProfile, SignalMessage}; use wzp_proto::{MediaTransport, QualityProfile, SignalMessage, default_signal_version};
/// Accept the relay (callee) side of the cryptographic handshake. /// Accept the relay (callee) side of the cryptographic handshake.
/// ///
@@ -51,6 +51,7 @@ pub async fn accept_handshake(
alias, alias,
protocol_version, protocol_version,
supported_versions: _, supported_versions: _,
..
} => ( } => (
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
@@ -70,6 +71,7 @@ pub async fn accept_handshake(
// 1a. Protocol version check — we only speak v2. // 1a. Protocol version check — we only speak v2.
if protocol_version != 2 { if protocol_version != 2 {
let mismatch = SignalMessage::Hangup { let mismatch = SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::ProtocolVersionMismatch { reason: wzp_proto::HangupReason::ProtocolVersionMismatch {
server_supported: vec![2], server_supported: vec![2],
}, },
@@ -108,6 +110,7 @@ pub async fn accept_handshake(
// 6. Send CallAnswer // 6. Send CallAnswer
let answer = SignalMessage::CallAnswer { let answer = SignalMessage::CallAnswer {
version: default_signal_version(),
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature, signature,

View File

@@ -16,7 +16,7 @@ use clap::Parser;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_relay::config::RelayConfig; use wzp_relay::config::RelayConfig;
use wzp_relay::metrics::RelayMetrics; use wzp_relay::metrics::RelayMetrics;
use wzp_relay::pipeline::{PipelineConfig, RelayPipeline}; use wzp_relay::pipeline::{PipelineConfig, RelayPipeline};
@@ -640,6 +640,7 @@ async fn main() -> anyhow::Result<()> {
.send_to( .send_to(
&caller_fp, &caller_fp,
&SignalMessage::Hangup { &SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}, },
@@ -685,6 +686,7 @@ async fn main() -> anyhow::Result<()> {
// Emit the LOCAL CallSetup to our local caller. // Emit the LOCAL CallSetup to our local caller.
let setup = SignalMessage::CallSetup { let setup = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: room_name.clone(), room: room_name.clone(),
relay_addr: advertised_addr_d.clone(), relay_addr: advertised_addr_d.clone(),
@@ -703,7 +705,7 @@ async fn main() -> anyhow::Result<()> {
); );
} }
SignalMessage::CallRinging { ref call_id } => { SignalMessage::CallRinging { ref call_id, .. } => {
// Forward to local caller for "ringing..." UX. // Forward to local caller for "ringing..." UX.
let caller_fp = { let caller_fp = {
let reg = call_registry_d.lock().await; let reg = call_registry_d.lock().await;
@@ -866,9 +868,12 @@ async fn main() -> anyhow::Result<()> {
info!(%addr, "probe connection detected, entering Ping/Pong + presence responder"); info!(%addr, "probe connection detected, entering Ping/Pong + presence responder");
loop { loop {
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(wzp_proto::SignalMessage::Ping { timestamp_ms })) => { Ok(Some(wzp_proto::SignalMessage::Ping { timestamp_ms, .. })) => {
if let Err(e) = transport if let Err(e) = transport
.send_signal(&wzp_proto::SignalMessage::Pong { timestamp_ms }) .send_signal(&wzp_proto::SignalMessage::Pong {
version: default_signal_version(),
timestamp_ms,
})
.await .await
{ {
error!(%addr, "probe pong send error: {e}"); error!(%addr, "probe pong send error: {e}");
@@ -878,6 +883,7 @@ async fn main() -> anyhow::Result<()> {
Ok(Some(wzp_proto::SignalMessage::PresenceUpdate { Ok(Some(wzp_proto::SignalMessage::PresenceUpdate {
fingerprints, fingerprints,
relay_addr, relay_addr,
..
})) => { })) => {
// A peer relay is telling us which fingerprints it has // A peer relay is telling us which fingerprints it has
let peer_addr: std::net::SocketAddr = let peer_addr: std::net::SocketAddr =
@@ -894,6 +900,7 @@ async fn main() -> anyhow::Result<()> {
reg.local_fingerprints().into_iter().collect() reg.local_fingerprints().into_iter().collect()
}; };
let reply = wzp_proto::SignalMessage::PresenceUpdate { let reply = wzp_proto::SignalMessage::PresenceUpdate {
version: default_signal_version(),
fingerprints: local_fps, fingerprints: local_fps,
relay_addr: addr.to_string(), relay_addr: addr.to_string(),
}; };
@@ -902,7 +909,9 @@ async fn main() -> anyhow::Result<()> {
break; break;
} }
} }
Ok(Some(wzp_proto::SignalMessage::RouteQuery { fingerprint, ttl })) => { Ok(Some(wzp_proto::SignalMessage::RouteQuery {
fingerprint, ttl, ..
})) => {
// Look up the fingerprint in our local registry // Look up the fingerprint in our local registry
let reg = presence.lock().await; let reg = presence.lock().await;
let route = route_resolver.resolve(&reg, &fingerprint); let route = route_resolver.resolve(&reg, &fingerprint);
@@ -930,6 +939,7 @@ async fn main() -> anyhow::Result<()> {
}; };
let reply = wzp_proto::SignalMessage::RouteResponse { let reply = wzp_proto::SignalMessage::RouteResponse {
version: default_signal_version(),
fingerprint, fingerprint,
found, found,
relay_chain, relay_chain,
@@ -968,6 +978,7 @@ async fn main() -> anyhow::Result<()> {
{ {
Ok(Ok(Some(wzp_proto::SignalMessage::FederationHello { Ok(Ok(Some(wzp_proto::SignalMessage::FederationHello {
tls_fingerprint, tls_fingerprint,
..
}))) => tls_fingerprint, }))) => tls_fingerprint,
_ => { _ => {
warn!(%addr, "federation: no hello received, closing"); warn!(%addr, "federation: no hello received, closing");
@@ -1004,7 +1015,7 @@ async fn main() -> anyhow::Result<()> {
// Optional auth // Optional auth
let auth_fp: Option<String> = if let Some(ref url) = auth_url { let auth_fp: Option<String> = if let Some(ref url) = auth_url {
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(SignalMessage::AuthToken { token })) => { Ok(Some(SignalMessage::AuthToken { token, .. })) => {
match wzp_relay::auth::validate_token(url, &token).await { match wzp_relay::auth::validate_token(url, &token).await {
Ok(client) => Some(client.fingerprint), Ok(client) => Some(client.fingerprint),
Err(e) => { Err(e) => {
@@ -1033,6 +1044,7 @@ async fn main() -> anyhow::Result<()> {
identity_pub, identity_pub,
signature: _, signature: _,
alias, alias,
..
}))) => { }))) => {
// Compute fingerprint: SHA-256(Ed25519 pub key)[:16], same as Fingerprint type // Compute fingerprint: SHA-256(Ed25519 pub key)[:16], same as Fingerprint type
let fp = { let fp = {
@@ -1067,6 +1079,7 @@ async fn main() -> anyhow::Result<()> {
// Send ack // Send ack
let _ = transport let _ = transport
.send_signal(&SignalMessage::RegisterPresenceAck { .send_signal(&SignalMessage::RegisterPresenceAck {
version: default_signal_version(),
success: true, success: true,
error: None, error: None,
relay_build: Some(BUILD_GIT_HASH.to_string()), relay_build: Some(BUILD_GIT_HASH.to_string()),
@@ -1126,6 +1139,7 @@ async fn main() -> anyhow::Result<()> {
// federation has a matching entry. // federation has a matching entry.
let forwarded = if let Some(ref fm) = federation_mgr { let forwarded = if let Some(ref fm) = federation_mgr {
let forward = SignalMessage::FederatedSignalForward { let forward = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(msg.clone()), inner: Box::new(msg.clone()),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1149,6 +1163,7 @@ async fn main() -> anyhow::Result<()> {
info!(%addr, target = %target_fp, "call target not online (no federation route)"); info!(%addr, target = %target_fp, "call target not online (no federation route)");
let _ = transport let _ = transport
.send_signal(&SignalMessage::Hangup { .send_signal(&SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}) })
@@ -1193,6 +1208,7 @@ async fn main() -> anyhow::Result<()> {
// federated delivery is in flight. // federated delivery is in flight.
let _ = transport let _ = transport
.send_signal(&SignalMessage::CallRinging { .send_signal(&SignalMessage::CallRinging {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
}) })
.await; .await;
@@ -1236,6 +1252,7 @@ async fn main() -> anyhow::Result<()> {
drop(hub); drop(hub);
let _ = transport let _ = transport
.send_signal(&SignalMessage::CallRinging { .send_signal(&SignalMessage::CallRinging {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
}) })
.await; .await;
@@ -1293,11 +1310,13 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref origin_fp) = peer_relay_fp { if let Some(ref origin_fp) = peer_relay_fp {
if let Some(ref fm) = federation_mgr { if let Some(ref fm) = federation_mgr {
let hangup = SignalMessage::Hangup { let hangup = SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: Some(call_id.clone()), call_id: Some(call_id.clone()),
}; };
let forward = let forward =
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(hangup), inner: Box::new(hangup),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1314,6 +1333,7 @@ async fn main() -> anyhow::Result<()> {
.send_to( .send_to(
&peer_fp, &peer_fp,
&SignalMessage::Hangup { &SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: Some(call_id.clone()), call_id: Some(call_id.clone()),
}, },
@@ -1390,6 +1410,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref fm) = federation_mgr { if let Some(ref fm) = federation_mgr {
let forward = let forward =
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(msg.clone()), inner: Box::new(msg.clone()),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1407,6 +1428,7 @@ async fn main() -> anyhow::Result<()> {
} }
let setup_for_callee = SignalMessage::CallSetup { let setup_for_callee = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: room.clone(), room: room.clone(),
relay_addr: relay_addr_for_setup, relay_addr: relay_addr_for_setup,
@@ -1429,6 +1451,7 @@ async fn main() -> anyhow::Result<()> {
// cross-wired candidates (Phase 5.5 ICE // cross-wired candidates (Phase 5.5 ICE
// + Phase 8 port-mapped addrs). // + Phase 8 port-mapped addrs).
let setup_for_caller = SignalMessage::CallSetup { let setup_for_caller = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: room.clone(), room: room.clone(),
relay_addr: relay_addr_for_setup.clone(), relay_addr: relay_addr_for_setup.clone(),
@@ -1437,6 +1460,7 @@ async fn main() -> anyhow::Result<()> {
peer_mapped_addr: callee_mapped, peer_mapped_addr: callee_mapped,
}; };
let setup_for_callee = SignalMessage::CallSetup { let setup_for_callee = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: room.clone(), room: room.clone(),
relay_addr: relay_addr_for_setup, relay_addr: relay_addr_for_setup,
@@ -1524,6 +1548,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref fm) = federation_mgr { if let Some(ref fm) = federation_mgr {
let forward = let forward =
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(msg.clone()), inner: Box::new(msg.clone()),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1568,6 +1593,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref fm) = federation_mgr { if let Some(ref fm) = federation_mgr {
let forward = let forward =
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(msg.clone()), inner: Box::new(msg.clone()),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1615,6 +1641,7 @@ async fn main() -> anyhow::Result<()> {
if let Some(ref fm) = federation_mgr { if let Some(ref fm) = federation_mgr {
let forward = let forward =
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(msg.clone()), inner: Box::new(msg.clone()),
origin_relay_fp: tls_fp.clone(), origin_relay_fp: tls_fp.clone(),
}; };
@@ -1629,9 +1656,12 @@ async fn main() -> anyhow::Result<()> {
} }
} }
SignalMessage::Ping { timestamp_ms } => { SignalMessage::Ping { timestamp_ms, .. } => {
let _ = transport let _ = transport
.send_signal(&SignalMessage::Pong { timestamp_ms }) .send_signal(&SignalMessage::Pong {
version: default_signal_version(),
timestamp_ms,
})
.await; .await;
} }
@@ -1651,6 +1681,7 @@ async fn main() -> anyhow::Result<()> {
let observed_addr = addr.to_string(); let observed_addr = addr.to_string();
if let Err(e) = transport if let Err(e) = transport
.send_signal(&SignalMessage::ReflectResponse { .send_signal(&SignalMessage::ReflectResponse {
version: default_signal_version(),
observed_addr: observed_addr.clone(), observed_addr: observed_addr.clone(),
}) })
.await .await
@@ -1710,6 +1741,7 @@ async fn main() -> anyhow::Result<()> {
.send_to( .send_to(
peer_fp, peer_fp,
&SignalMessage::Hangup { &SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: Some(call_id.clone()), call_id: Some(call_id.clone()),
}, },
@@ -1741,7 +1773,7 @@ async fn main() -> anyhow::Result<()> {
let authenticated_fp: Option<String> = if let Some(ref url) = auth_url { let authenticated_fp: Option<String> = if let Some(ref url) = auth_url {
info!(%addr, "waiting for auth token..."); info!(%addr, "waiting for auth token...");
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(wzp_proto::SignalMessage::AuthToken { token })) => { Ok(Some(wzp_proto::SignalMessage::AuthToken { token, .. })) => {
match wzp_relay::auth::validate_token(url, &token).await { match wzp_relay::auth::validate_token(url, &token).await {
Ok(client) => { Ok(client) => {
metrics.auth_attempts.with_label_values(&["ok"]).inc(); metrics.auth_attempts.with_label_values(&["ok"]).inc();
@@ -1913,6 +1945,7 @@ async fn main() -> anyhow::Result<()> {
if let SignalMessage::RoomUpdate { if let SignalMessage::RoomUpdate {
count: _, count: _,
participants: mut local_parts, participants: mut local_parts,
..
} = update } = update
{ {
let remote = fm.get_remote_participants(&room_name).await; let remote = fm.get_remote_participants(&room_name).await;
@@ -1921,6 +1954,7 @@ async fn main() -> anyhow::Result<()> {
let mut seen = std::collections::HashSet::new(); let mut seen = std::collections::HashSet::new();
local_parts.retain(|p| seen.insert(p.fingerprint.clone())); local_parts.retain(|p| seen.insert(p.fingerprint.clone()));
SignalMessage::RoomUpdate { SignalMessage::RoomUpdate {
version: default_signal_version(),
count: local_parts.len() as u32, count: local_parts.len() as u32,
participants: local_parts, participants: local_parts,
} }

View File

@@ -13,7 +13,7 @@ use prometheus::{Gauge, IntGauge, Opts, Registry};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
/// Configuration for a single probe target. /// Configuration for a single probe target.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -229,7 +229,7 @@ impl ProbeRunner {
let recv_handle = tokio::spawn(async move { let recv_handle = tokio::spawn(async move {
loop { loop {
match recv_transport.recv_signal().await { match recv_transport.recv_signal().await {
Ok(Some(SignalMessage::Pong { timestamp_ms })) => { Ok(Some(SignalMessage::Pong { timestamp_ms, .. })) => {
let now_ms = SystemTime::now() let now_ms = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
@@ -244,6 +244,7 @@ impl ProbeRunner {
Ok(Some(SignalMessage::PresenceUpdate { Ok(Some(SignalMessage::PresenceUpdate {
fingerprints, fingerprints,
relay_addr, relay_addr,
..
})) => { })) => {
if let Some(ref reg) = recv_presence { if let Some(ref reg) = recv_presence {
// Parse the relay_addr; fall back to the connection target // Parse the relay_addr; fall back to the connection target
@@ -293,7 +294,10 @@ impl ProbeRunner {
} }
if let Err(e) = transport if let Err(e) = transport
.send_signal(&SignalMessage::Ping { timestamp_ms }) .send_signal(&SignalMessage::Ping {
version: default_signal_version(),
timestamp_ms,
})
.await .await
{ {
error!(target = %self.config.target, "probe ping send error: {e}"); error!(target = %self.config.target, "probe ping send error: {e}");
@@ -310,6 +314,7 @@ impl ProbeRunner {
r.local_fingerprints().into_iter().collect() r.local_fingerprints().into_iter().collect()
}; };
let msg = SignalMessage::PresenceUpdate { let msg = SignalMessage::PresenceUpdate {
version: default_signal_version(),
fingerprints: fps, fingerprints: fps,
relay_addr: self.config.target.to_string(), relay_addr: self.config.target.to_string(),
}; };
@@ -426,9 +431,10 @@ pub fn mesh_summary(registry: &Registry) -> String {
/// Handle an incoming Ping signal by replying with a Pong carrying the same timestamp. /// Handle an incoming Ping signal by replying with a Pong carrying the same timestamp.
/// Returns true if the message was a Ping and was handled, false otherwise. /// Returns true if the message was a Ping and was handled, false otherwise.
pub async fn handle_ping(transport: &wzp_transport::QuinnTransport, msg: &SignalMessage) -> bool { pub async fn handle_ping(transport: &wzp_transport::QuinnTransport, msg: &SignalMessage) -> bool {
if let SignalMessage::Ping { timestamp_ms } = msg { if let SignalMessage::Ping { timestamp_ms, .. } = msg {
if let Err(e) = transport if let Err(e) = transport
.send_signal(&SignalMessage::Pong { .send_signal(&SignalMessage::Pong {
version: default_signal_version(),
timestamp_ms: *timestamp_ms, timestamp_ms: *timestamp_ms,
}) })
.await .await

View File

@@ -333,10 +333,11 @@ mod tests {
#[test] #[test]
fn session_forward_signal_roundtrip() { fn session_forward_signal_roundtrip() {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
// SessionForward roundtrip // SessionForward roundtrip
let msg = SignalMessage::SessionForward { let msg = SignalMessage::SessionForward {
version: default_signal_version(),
session_id: "abcd1234".to_string(), session_id: "abcd1234".to_string(),
target_fingerprint: "deadbeef".to_string(), target_fingerprint: "deadbeef".to_string(),
source_relay: "10.0.0.1:4433".to_string(), source_relay: "10.0.0.1:4433".to_string(),
@@ -348,6 +349,7 @@ mod tests {
session_id, session_id,
target_fingerprint, target_fingerprint,
source_relay, source_relay,
..
} => { } => {
assert_eq!(session_id, "abcd1234"); assert_eq!(session_id, "abcd1234");
assert_eq!(target_fingerprint, "deadbeef"); assert_eq!(target_fingerprint, "deadbeef");
@@ -358,6 +360,7 @@ mod tests {
// SessionForwardAck roundtrip // SessionForwardAck roundtrip
let ack = SignalMessage::SessionForwardAck { let ack = SignalMessage::SessionForwardAck {
version: default_signal_version(),
session_id: "abcd1234".to_string(), session_id: "abcd1234".to_string(),
room_name: "relay-room-42".to_string(), room_name: "relay-room-42".to_string(),
}; };
@@ -367,6 +370,7 @@ mod tests {
SignalMessage::SessionForwardAck { SignalMessage::SessionForwardAck {
session_id, session_id,
room_name, room_name,
..
} => { } => {
assert_eq!(session_id, "abcd1234"); assert_eq!(session_id, "abcd1234");
assert_eq!(room_name, "relay-room-42"); assert_eq!(room_name, "relay-room-42");

View File

@@ -13,10 +13,10 @@ use bytes::Bytes;
use dashmap::DashMap; use dashmap::DashMap;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use wzp_proto::MediaTransport;
use wzp_proto::packet::TrunkFrame; use wzp_proto::packet::TrunkFrame;
use wzp_proto::quality::{AdaptiveQualityController, Tier}; use wzp_proto::quality::{AdaptiveQualityController, Tier};
use wzp_proto::traits::QualityController; use wzp_proto::traits::QualityController;
use wzp_proto::{MediaTransport, default_signal_version};
use crate::conformance::ConformanceMeter; use crate::conformance::ConformanceMeter;
use crate::metrics::RelayMetrics; use crate::metrics::RelayMetrics;
@@ -64,6 +64,7 @@ impl DebugTap {
wzp_proto::SignalMessage::RoomUpdate { wzp_proto::SignalMessage::RoomUpdate {
count, count,
participants, participants,
..
} => { } => {
let names: Vec<&str> = participants let names: Vec<&str> = participants
.iter() .iter()
@@ -81,6 +82,7 @@ impl DebugTap {
wzp_proto::SignalMessage::QualityDirective { wzp_proto::SignalMessage::QualityDirective {
recommended_profile, recommended_profile,
reason, reason,
..
} => { } => {
info!( info!(
target: "debug_tap", target: "debug_tap",
@@ -493,6 +495,7 @@ impl RoomManager {
); );
room.qualities.insert(id, ParticipantQuality::new()); room.qualities.insert(id, ParticipantQuality::new());
let update = wzp_proto::SignalMessage::RoomUpdate { let update = wzp_proto::SignalMessage::RoomUpdate {
version: default_signal_version(),
count: room.len() as u32, count: room.len() as u32,
participants: room.participant_list(), participants: room.participant_list(),
}; };
@@ -570,6 +573,7 @@ impl RoomManager {
return None; return None;
} }
let update = wzp_proto::SignalMessage::RoomUpdate { let update = wzp_proto::SignalMessage::RoomUpdate {
version: default_signal_version(),
count: room.len() as u32, count: room.len() as u32,
participants: room.participant_list(), participants: room.participant_list(),
}; };
@@ -654,6 +658,7 @@ impl RoomManager {
); );
let directive = wzp_proto::SignalMessage::QualityDirective { let directive = wzp_proto::SignalMessage::QualityDirective {
version: default_signal_version(),
recommended_profile: profile, recommended_profile: profile,
reason: Some(format!("weakest link: {weakest:?}")), reason: Some(format!("weakest link: {weakest:?}")),
}; };

View File

@@ -201,9 +201,10 @@ mod tests {
#[test] #[test]
fn route_query_signal_roundtrip() { fn route_query_signal_roundtrip() {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
let query = SignalMessage::RouteQuery { let query = SignalMessage::RouteQuery {
version: default_signal_version(),
fingerprint: "aabbccdd".to_string(), fingerprint: "aabbccdd".to_string(),
ttl: 3, ttl: 3,
}; };
@@ -211,11 +212,12 @@ mod tests {
let decoded: SignalMessage = serde_json::from_str(&json).unwrap(); let decoded: SignalMessage = serde_json::from_str(&json).unwrap();
assert!(matches!( assert!(matches!(
decoded, decoded,
SignalMessage::RouteQuery { ref fingerprint, ttl } SignalMessage::RouteQuery { ref fingerprint, ttl, ..}
if fingerprint == "aabbccdd" && ttl == 3 if fingerprint == "aabbccdd" && ttl == 3
)); ));
let response = SignalMessage::RouteResponse { let response = SignalMessage::RouteResponse {
version: default_signal_version(),
fingerprint: "aabbccdd".to_string(), fingerprint: "aabbccdd".to_string(),
found: true, found: true,
relay_chain: vec!["10.0.0.1:4433".to_string(), "10.0.0.2:4433".to_string()], relay_chain: vec!["10.0.0.1:4433".to_string(), "10.0.0.2:4433".to_string()],
@@ -224,7 +226,7 @@ mod tests {
let decoded: SignalMessage = serde_json::from_str(&json).unwrap(); let decoded: SignalMessage = serde_json::from_str(&json).unwrap();
assert!(matches!( assert!(matches!(
decoded, decoded,
SignalMessage::RouteResponse { ref fingerprint, found, ref relay_chain } SignalMessage::RouteResponse { ref fingerprint, found, ref relay_chain, ..}
if fingerprint == "aabbccdd" && found && relay_chain.len() == 2 if fingerprint == "aabbccdd" && found && relay_chain.len() == 2
)); ));
} }

View File

@@ -8,7 +8,7 @@ use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use tracing::info; use tracing::info;
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_transport::QuinnTransport; use wzp_transport::QuinnTransport;
/// A client connected via `_signal` for direct calling. /// A client connected via `_signal` for direct calling.
@@ -101,7 +101,10 @@ impl SignalHub {
alias: c.alias.clone(), alias: c.alias.clone(),
}) })
.collect(); .collect();
SignalMessage::PresenceList { users } SignalMessage::PresenceList {
version: default_signal_version(),
users,
}
} }
/// Broadcast a message to ALL connected signal clients. /// Broadcast a message to ALL connected signal clients.

View File

@@ -24,7 +24,7 @@
//! Bob's CallSetup carries Alice's reflex addr — cross-wired //! Bob's CallSetup carries Alice's reflex addr — cross-wired
//! through two relays + a federation link. //! through two relays + a federation link.
use wzp_proto::{CallAcceptMode, SignalMessage}; use wzp_proto::{CallAcceptMode, SignalMessage, default_signal_version};
use wzp_relay::call_registry::CallRegistry; use wzp_relay::call_registry::CallRegistry;
// ──────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────
@@ -42,6 +42,7 @@ const RELAY_B_ADDR: &str = "203.0.113.10:4433";
/// Helper that Alice's place_call sends. /// Helper that Alice's place_call sends.
fn alice_offer(call_id: &str) -> SignalMessage { fn alice_offer(call_id: &str) -> SignalMessage {
SignalMessage::DirectCallOffer { SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -84,6 +85,7 @@ fn relay_a_handle_offer(reg_a: &mut CallRegistry, offer: &SignalMessage) -> Sign
// Build the federation envelope the main loop would // Build the federation envelope the main loop would
// broadcast. // broadcast.
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(offer.clone()), inner: Box::new(offer.clone()),
origin_relay_fp: RELAY_A_TLS_FP.into(), origin_relay_fp: RELAY_A_TLS_FP.into(),
} }
@@ -97,6 +99,7 @@ fn relay_b_handle_forwarded_offer(reg_b: &mut CallRegistry, forward: &SignalMess
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
inner, inner,
origin_relay_fp, origin_relay_fp,
..
} => (inner.as_ref().clone(), origin_relay_fp.clone()), } => (inner.as_ref().clone(), origin_relay_fp.clone()),
_ => panic!("not a forward"), _ => panic!("not a forward"),
}; };
@@ -123,6 +126,7 @@ fn relay_b_handle_forwarded_offer(reg_b: &mut CallRegistry, forward: &SignalMess
/// Bob's answer — AcceptTrusted with his reflex addr. /// Bob's answer — AcceptTrusted with his reflex addr.
fn bob_answer(call_id: &str) -> SignalMessage { fn bob_answer(call_id: &str) -> SignalMessage {
SignalMessage::DirectCallAnswer { SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: call_id.into(), call_id: call_id.into(),
accept_mode: CallAcceptMode::AcceptTrusted, accept_mode: CallAcceptMode::AcceptTrusted,
identity_pub: None, identity_pub: None,
@@ -166,12 +170,14 @@ fn relay_b_handle_local_answer(
// Forward the answer back over federation. // Forward the answer back over federation.
let forward = SignalMessage::FederatedSignalForward { let forward = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(answer.clone()), inner: Box::new(answer.clone()),
origin_relay_fp: RELAY_B_TLS_FP.into(), origin_relay_fp: RELAY_B_TLS_FP.into(),
}; };
// Local CallSetup for Bob — peer_direct_addr = Alice's addr. // Local CallSetup for Bob — peer_direct_addr = Alice's addr.
let setup_for_bob = SignalMessage::CallSetup { let setup_for_bob = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: format!("call-{call_id}"), room: format!("call-{call_id}"),
relay_addr: RELAY_B_ADDR.into(), relay_addr: RELAY_B_ADDR.into(),
@@ -194,6 +200,7 @@ fn relay_a_handle_forwarded_answer(
SignalMessage::FederatedSignalForward { SignalMessage::FederatedSignalForward {
inner, inner,
origin_relay_fp, origin_relay_fp,
..
} => (inner.as_ref().clone(), origin_relay_fp.clone()), } => (inner.as_ref().clone(), origin_relay_fp.clone()),
_ => panic!("not a forward"), _ => panic!("not a forward"),
}; };
@@ -215,6 +222,7 @@ fn relay_a_handle_forwarded_answer(
// Alice's CallSetup — peer_direct_addr = Bob's addr. // Alice's CallSetup — peer_direct_addr = Bob's addr.
SignalMessage::CallSetup { SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: format!("call-{call_id}"), room: format!("call-{call_id}"),
relay_addr: RELAY_A_ADDR.into(), relay_addr: RELAY_A_ADDR.into(),
@@ -312,6 +320,7 @@ fn cross_relay_loop_prevention_drops_self_sourced_forward() {
// A FederatedSignalForward that circles back to the origin // A FederatedSignalForward that circles back to the origin
// relay should be dropped before it hits the call registry. // relay should be dropped before it hits the call registry.
let forward = SignalMessage::FederatedSignalForward { let forward = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(alice_offer("c-loop")), inner: Box::new(alice_offer("c-loop")),
origin_relay_fp: RELAY_B_TLS_FP.into(), origin_relay_fp: RELAY_B_TLS_FP.into(),
}; };

View File

@@ -18,7 +18,7 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use bytes::Bytes; use bytes::Bytes;
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_relay::config::{PeerConfig, TrustedConfig}; use wzp_relay::config::{PeerConfig, TrustedConfig};
use wzp_relay::event_log::EventLogger; use wzp_relay::event_log::EventLogger;
use wzp_relay::federation::{FederationManager, room_hash}; use wzp_relay::federation::{FederationManager, room_hash};
@@ -348,7 +348,9 @@ async fn broadcast_signal_sends_to_all_peers() {
.expect("some message"); .expect("some message");
match hello { match hello {
SignalMessage::FederationHello { tls_fingerprint } => { SignalMessage::FederationHello {
tls_fingerprint, ..
} => {
assert_eq!(tls_fingerprint, "test-relay-fp-abc123"); assert_eq!(tls_fingerprint, "test-relay-fp-abc123");
} }
other => panic!( other => panic!(
@@ -367,6 +369,7 @@ async fn broadcast_signal_sends_to_all_peers() {
// Now call broadcast_signal on the FM // Now call broadcast_signal on the FM
let test_msg = SignalMessage::FederatedSignalForward { let test_msg = SignalMessage::FederatedSignalForward {
version: default_signal_version(),
inner: Box::new(SignalMessage::Reflect), inner: Box::new(SignalMessage::Reflect),
origin_relay_fp: "other-relay-fp".into(), origin_relay_fp: "other-relay-fp".into(),
}; };

View File

@@ -9,7 +9,7 @@ use std::sync::Arc;
use wzp_client::perform_handshake; use wzp_client::perform_handshake;
use wzp_crypto::{KeyExchange, WarzoneKeyExchange}; use wzp_crypto::{KeyExchange, WarzoneKeyExchange};
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_relay::handshake::accept_handshake; use wzp_relay::handshake::accept_handshake;
use wzp_transport::{QuinnTransport, client_config, create_endpoint, server_config}; use wzp_transport::{QuinnTransport, client_config, create_endpoint, server_config};
@@ -129,6 +129,7 @@ async fn handshake_rejects_v1_protocol_version() {
let signature = kx.sign(&sign_data); let signature = kx.sign(&sign_data);
let v1_offer = SignalMessage::CallOffer { let v1_offer = SignalMessage::CallOffer {
version: 1,
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature, signature,
@@ -255,7 +256,7 @@ async fn auth_then_handshake() {
.expect("should receive a message"); .expect("should receive a message");
let token = match auth_msg { let token = match auth_msg {
SignalMessage::AuthToken { token } => token, SignalMessage::AuthToken { token, .. } => token,
other => panic!( other => panic!(
"expected AuthToken, got {:?}", "expected AuthToken, got {:?}",
std::mem::discriminant(&other) std::mem::discriminant(&other)
@@ -273,6 +274,7 @@ async fn auth_then_handshake() {
// Caller side: send AuthToken first, then perform_handshake. // Caller side: send AuthToken first, then perform_handshake.
let auth = SignalMessage::AuthToken { let auth = SignalMessage::AuthToken {
version: default_signal_version(),
token: "bearer-test-token-12345".to_string(), token: "bearer-test-token-12345".to_string(),
}; };
client_transport client_transport
@@ -344,6 +346,7 @@ async fn handshake_rejects_bad_signature() {
} }
let bad_offer = SignalMessage::CallOffer { let bad_offer = SignalMessage::CallOffer {
version: default_signal_version(),
identity_pub, identity_pub,
ephemeral_pub, ephemeral_pub,
signature, signature,

View File

@@ -20,7 +20,7 @@
//! to reason about, no real network, and what we actually want to //! to reason about, no real network, and what we actually want to
//! test is the cross-wiring logic, not the whole signal stack. //! test is the cross-wiring logic, not the whole signal stack.
use wzp_proto::{CallAcceptMode, SignalMessage}; use wzp_proto::{CallAcceptMode, SignalMessage, default_signal_version};
use wzp_relay::call_registry::CallRegistry; use wzp_relay::call_registry::CallRegistry;
/// Helper: simulate the relay's handling of a DirectCallOffer. In /// Helper: simulate the relay's handling of a DirectCallOffer. In
@@ -77,6 +77,7 @@ fn handle_answer_and_build_setups(
}; };
let setup_for_caller = SignalMessage::CallSetup { let setup_for_caller = SignalMessage::CallSetup {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
room: room.clone(), room: room.clone(),
relay_addr: "203.0.113.5:4433".into(), relay_addr: "203.0.113.5:4433".into(),
@@ -85,6 +86,7 @@ fn handle_answer_and_build_setups(
peer_mapped_addr: None, peer_mapped_addr: None,
}; };
let setup_for_callee = SignalMessage::CallSetup { let setup_for_callee = SignalMessage::CallSetup {
version: default_signal_version(),
call_id, call_id,
room, room,
relay_addr: "203.0.113.5:4433".into(), relay_addr: "203.0.113.5:4433".into(),
@@ -97,6 +99,7 @@ fn handle_answer_and_build_setups(
fn mk_offer(call_id: &str, caller_reflexive_addr: Option<&str>) -> SignalMessage { fn mk_offer(call_id: &str, caller_reflexive_addr: Option<&str>) -> SignalMessage {
SignalMessage::DirectCallOffer { SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: "alice".into(), caller_fingerprint: "alice".into(),
caller_alias: None, caller_alias: None,
target_fingerprint: "bob".into(), target_fingerprint: "bob".into(),
@@ -118,6 +121,7 @@ fn mk_answer(
callee_reflexive_addr: Option<&str>, callee_reflexive_addr: Option<&str>,
) -> SignalMessage { ) -> SignalMessage {
SignalMessage::DirectCallAnswer { SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: call_id.into(), call_id: call_id.into(),
accept_mode: mode, accept_mode: mode,
identity_pub: None, identity_pub: None,

View File

@@ -63,6 +63,7 @@ async fn spawn_mock_relay() -> (SocketAddr, tokio::task::JoinHandle<()>) {
Ok(Some(SignalMessage::RegisterPresence { .. })) => { Ok(Some(SignalMessage::RegisterPresence { .. })) => {
let _ = t let _ = t
.send_signal(&SignalMessage::RegisterPresenceAck { .send_signal(&SignalMessage::RegisterPresenceAck {
version: 1,
success: true, success: true,
error: None, error: None,
relay_build: None, relay_build: None,
@@ -74,6 +75,7 @@ async fn spawn_mock_relay() -> (SocketAddr, tokio::task::JoinHandle<()>) {
Ok(Some(SignalMessage::Reflect)) => { Ok(Some(SignalMessage::Reflect)) => {
let _ = t let _ = t
.send_signal(&SignalMessage::ReflectResponse { .send_signal(&SignalMessage::ReflectResponse {
version: 1,
observed_addr: observed_addr.to_string(), observed_addr: observed_addr.to_string(),
}) })
.await; .await;

View File

@@ -30,7 +30,7 @@ use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use wzp_proto::{MediaTransport, SignalMessage}; use wzp_proto::{MediaTransport, SignalMessage, default_signal_version};
use wzp_transport::{QuinnTransport, client_config, create_endpoint, server_config}; use wzp_transport::{QuinnTransport, client_config, create_endpoint, server_config};
/// Spawn a minimal mock relay that loops over `recv_signal`, /// Spawn a minimal mock relay that loops over `recv_signal`,
@@ -49,6 +49,7 @@ async fn spawn_mock_relay_with_reflect(
match server_transport.recv_signal().await { match server_transport.recv_signal().await {
Ok(Some(SignalMessage::Reflect)) => { Ok(Some(SignalMessage::Reflect)) => {
let resp = SignalMessage::ReflectResponse { let resp = SignalMessage::ReflectResponse {
version: default_signal_version(),
observed_addr: observed.to_string(), observed_addr: observed.to_string(),
}; };
// If the send fails the client has gone; just exit. // If the send fails the client has gone; just exit.
@@ -164,7 +165,7 @@ async fn reflect_happy_path() {
.expect("some message"); .expect("some message");
let observed_addr = match resp { let observed_addr = match resp {
SignalMessage::ReflectResponse { observed_addr } => observed_addr, SignalMessage::ReflectResponse { observed_addr, .. } => observed_addr,
other => panic!( other => panic!(
"expected ReflectResponse, got {:?}", "expected ReflectResponse, got {:?}",
std::mem::discriminant(&other) std::mem::discriminant(&other)
@@ -251,7 +252,7 @@ async fn reflect_two_clients_distinct_ports() {
.expect("ok") .expect("ok")
.expect("some"); .expect("some");
match resp { match resp {
SignalMessage::ReflectResponse { observed_addr } => observed_addr, SignalMessage::ReflectResponse { observed_addr, .. } => observed_addr,
_ => panic!("wrong variant"), _ => panic!("wrong variant"),
} }
}; };

View File

@@ -22,7 +22,7 @@ use tower_http::services::ServeDir;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use wzp_client::call::{CallConfig, CallDecoder, CallEncoder}; use wzp_client::call::{CallConfig, CallDecoder, CallEncoder};
use wzp_proto::MediaTransport; use wzp_proto::{MediaTransport, default_signal_version};
mod metrics; mod metrics;
use metrics::WebMetrics; use metrics::WebMetrics;
@@ -297,6 +297,7 @@ async fn handle_ws(socket: WebSocket, room: String, state: AppState) {
// Send auth token to relay (if auth is enabled) // Send auth token to relay (if auth is enabled)
if let Some(ref token) = browser_token { if let Some(ref token) = browser_token {
let auth = wzp_proto::SignalMessage::AuthToken { let auth = wzp_proto::SignalMessage::AuthToken {
version: default_signal_version(),
token: token.clone(), token: token.clone(),
}; };
if let Err(e) = transport.send_signal(&auth).await { if let Err(e) = transport.send_signal(&auth).await {

View File

@@ -168,6 +168,7 @@ async fn run_signal_task(
Ok(Ok(Some(wzp_proto::SignalMessage::QualityDirective { Ok(Ok(Some(wzp_proto::SignalMessage::QualityDirective {
recommended_profile, recommended_profile,
reason, reason,
..
}))) => { }))) => {
let idx = profile_to_index(&recommended_profile); let idx = profile_to_index(&recommended_profile);
info!( info!(

View File

@@ -35,7 +35,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use tauri::{Emitter, Manager}; use tauri::{Emitter, Manager};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use wzp_proto::MediaTransport; use wzp_proto::{MediaTransport, default_signal_version};
// ─── Call-flow debug logs (GUI-gated) ──────────────────────────────── // ─── Call-flow debug logs (GUI-gated) ────────────────────────────────
// //
@@ -615,6 +615,7 @@ async fn connect(
// Send our report // Send our report
if let Some(ref t) = transport_for_report { if let Some(ref t) = transport_for_report {
let report = wzp_proto::SignalMessage::MediaPathReport { let report = wzp_proto::SignalMessage::MediaPathReport {
version: default_signal_version(),
call_id: call_id_for_report.clone(), call_id: call_id_for_report.clone(),
direct_ok: local_direct_ok, direct_ok: local_direct_ok,
race_winner: format!("{:?}", local_winner), race_winner: format!("{:?}", local_winner),
@@ -1247,7 +1248,7 @@ fn do_register_signal(
relay: String, relay: String,
) -> impl std::future::Future<Output = Result<String, String>> + Send { ) -> impl std::future::Future<Output = Result<String, String>> + Send {
async move { async move {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
emit_call_debug( emit_call_debug(
&app, &app,
@@ -1316,6 +1317,7 @@ fn do_register_signal(
let alias = derive_alias(&seed); let alias = derive_alias(&seed);
transport transport
.send_signal(&SignalMessage::RegisterPresence { .send_signal(&SignalMessage::RegisterPresence {
version: default_signal_version(),
identity_pub, identity_pub,
signature: vec![], signature: vec![],
alias: Some(alias), alias: Some(alias),
@@ -1375,7 +1377,7 @@ fn do_register_signal(
let signal_state = signal_state_loop.clone(); let signal_state = signal_state_loop.clone();
loop { loop {
match transport.recv_signal().await { match transport.recv_signal().await {
Ok(Some(SignalMessage::CallRinging { call_id })) => { Ok(Some(SignalMessage::CallRinging { call_id, .. })) => {
tracing::info!(%call_id, "signal: CallRinging"); tracing::info!(%call_id, "signal: CallRinging");
emit_call_debug( emit_call_debug(
&app_clone, &app_clone,
@@ -1453,6 +1455,7 @@ fn do_register_signal(
peer_direct_addr, peer_direct_addr,
peer_local_addrs, peer_local_addrs,
peer_mapped_addr, peer_mapped_addr,
..
})) => { })) => {
// Phase 3: peer_direct_addr carries the OTHER party's // Phase 3: peer_direct_addr carries the OTHER party's
// reflex addr. Phase 5.5: peer_local_addrs carries // reflex addr. Phase 5.5: peer_local_addrs carries
@@ -1512,6 +1515,7 @@ fn do_register_signal(
call_id, call_id,
direct_ok, direct_ok,
race_winner, race_winner,
..
})) => { })) => {
// Phase 6: the peer is telling us whether // Phase 6: the peer is telling us whether
// their direct path succeeded. Fire the // their direct path succeeded. Fire the
@@ -1543,6 +1547,7 @@ fn do_register_signal(
local_addrs, local_addrs,
mapped_addr, mapped_addr,
generation, generation,
..
})) => { })) => {
// Phase 8: peer re-gathered candidates after a // Phase 8: peer re-gathered candidates after a
// network change. Emit to JS for UI notification // network change. Emit to JS for UI notification
@@ -1586,6 +1591,7 @@ fn do_register_signal(
allocation, allocation,
probe_time_ms, probe_time_ms,
external_ip, external_ip,
..
})) => { })) => {
tracing::info!( tracing::info!(
%call_id, %call_id,
@@ -1647,6 +1653,7 @@ fn do_register_signal(
let _ = t let _ = t
.send_signal( .send_signal(
&wzp_proto::SignalMessage::HardNatBirthdayStart { &wzp_proto::SignalMessage::HardNatBirthdayStart {
version: default_signal_version(),
call_id: call_id_bg, call_id: call_id_bg,
acceptor_port_count: result.succeeded, acceptor_port_count: result.succeeded,
acceptor_ports: ext_ports, acceptor_ports: ext_ports,
@@ -1661,7 +1668,7 @@ fn do_register_signal(
}); });
} }
} }
Ok(Some(SignalMessage::PresenceList { users })) => { Ok(Some(SignalMessage::PresenceList { users, .. })) => {
tracing::info!(count = users.len(), "signal: PresenceList received"); tracing::info!(count = users.len(), "signal: PresenceList received");
// Emit to JS frontend for lobby user list // Emit to JS frontend for lobby user list
let user_list: Vec<serde_json::Value> = users let user_list: Vec<serde_json::Value> = users
@@ -1687,6 +1694,7 @@ fn do_register_signal(
proposed_profile, proposed_profile,
local_loss_pct, local_loss_pct,
local_rtt_ms, local_rtt_ms,
..
})) => { })) => {
tracing::info!(%call_id, %proposal_id, ?proposed_profile, "signal: UpgradeProposal from peer"); tracing::info!(%call_id, %proposal_id, ?proposed_profile, "signal: UpgradeProposal from peer");
emit_call_debug( emit_call_debug(
@@ -1706,6 +1714,7 @@ fn do_register_signal(
proposal_id, proposal_id,
accepted, accepted,
reason, reason,
..
})) => { })) => {
tracing::info!(%call_id, %proposal_id, accepted, ?reason, "signal: UpgradeResponse from peer"); tracing::info!(%call_id, %proposal_id, accepted, ?reason, "signal: UpgradeResponse from peer");
emit_call_debug( emit_call_debug(
@@ -1722,6 +1731,7 @@ fn do_register_signal(
call_id, call_id,
proposal_id, proposal_id,
confirmed_profile, confirmed_profile,
..
})) => { })) => {
tracing::info!(%call_id, %proposal_id, ?confirmed_profile, "signal: UpgradeConfirm"); tracing::info!(%call_id, %proposal_id, ?confirmed_profile, "signal: UpgradeConfirm");
emit_call_debug( emit_call_debug(
@@ -1739,6 +1749,7 @@ fn do_register_signal(
max_profile, max_profile,
loss_pct, loss_pct,
rtt_ms, rtt_ms,
..
})) => { })) => {
tracing::info!(%call_id, ?max_profile, "signal: QualityCapability from peer"); tracing::info!(%call_id, ?max_profile, "signal: QualityCapability from peer");
emit_call_debug( emit_call_debug(
@@ -1758,6 +1769,7 @@ fn do_register_signal(
acceptor_port_count, acceptor_port_count,
acceptor_ports, acceptor_ports,
external_ip, external_ip,
..
})) => { })) => {
tracing::info!( tracing::info!(
%call_id, %call_id,
@@ -1786,7 +1798,7 @@ fn do_register_signal(
}); });
} }
} }
Ok(Some(SignalMessage::ReflectResponse { observed_addr })) => { Ok(Some(SignalMessage::ReflectResponse { observed_addr, .. })) => {
// "STUN for QUIC" response — the relay told us our // "STUN for QUIC" response — the relay told us our
// own server-reflexive address. If a Tauri command // own server-reflexive address. If a Tauri command
// is currently awaiting this, fire the oneshot; // is currently awaiting this, fire the oneshot;
@@ -2009,7 +2021,7 @@ async fn place_call(
app: tauri::AppHandle, app: tauri::AppHandle,
target_fp: String, target_fp: String,
) -> Result<(), String> { ) -> Result<(), String> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
emit_call_debug( emit_call_debug(
&app, &app,
@@ -2140,6 +2152,7 @@ async fn place_call(
tracing::info!(%call_id, %target_fp, reflex = ?own_reflex, mapped = ?caller_mapped_addr, "place_call: sending DirectCallOffer"); tracing::info!(%call_id, %target_fp, reflex = ?own_reflex, mapped = ?caller_mapped_addr, "place_call: sending DirectCallOffer");
transport transport
.send_signal(&SignalMessage::DirectCallOffer { .send_signal(&SignalMessage::DirectCallOffer {
version: default_signal_version(),
caller_fingerprint: sig.fingerprint.clone(), caller_fingerprint: sig.fingerprint.clone(),
caller_alias: None, caller_alias: None,
target_fingerprint: target_fp.clone(), target_fingerprint: target_fp.clone(),
@@ -2199,6 +2212,7 @@ async fn place_call(
if let Some(ref t) = sig.transport { if let Some(ref t) = sig.transport {
let _ = t let _ = t
.send_signal(&SignalMessage::HardNatProbe { .send_signal(&SignalMessage::HardNatProbe {
version: default_signal_version(),
call_id: call_id_bg, call_id: call_id_bg,
port_sequence: result.observed_ports, port_sequence: result.observed_ports,
allocation: alloc_str, allocation: alloc_str,
@@ -2228,7 +2242,7 @@ async fn answer_call(
call_id: String, call_id: String,
mode: i32, mode: i32,
) -> Result<(), String> { ) -> Result<(), String> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
let accept_mode = match mode { let accept_mode = match mode {
0 => wzp_proto::CallAcceptMode::Reject, 0 => wzp_proto::CallAcceptMode::Reject,
1 => wzp_proto::CallAcceptMode::AcceptTrusted, 1 => wzp_proto::CallAcceptMode::AcceptTrusted,
@@ -2375,6 +2389,7 @@ async fn answer_call(
tracing::info!(%call_id, ?accept_mode, reflex = ?own_reflex, mapped = ?callee_mapped_addr, "answer_call: sending DirectCallAnswer"); tracing::info!(%call_id, ?accept_mode, reflex = ?own_reflex, mapped = ?callee_mapped_addr, "answer_call: sending DirectCallAnswer");
transport transport
.send_signal(&SignalMessage::DirectCallAnswer { .send_signal(&SignalMessage::DirectCallAnswer {
version: default_signal_version(),
call_id: call_id.clone(), call_id: call_id.clone(),
accept_mode, accept_mode,
identity_pub: None, identity_pub: None,
@@ -2437,6 +2452,7 @@ async fn answer_call(
if let Some(ref t) = sig.transport { if let Some(ref t) = sig.transport {
let _ = t let _ = t
.send_signal(&wzp_proto::SignalMessage::HardNatProbe { .send_signal(&wzp_proto::SignalMessage::HardNatProbe {
version: default_signal_version(),
call_id: call_id_bg, call_id: call_id_bg,
port_sequence: result.observed_ports, port_sequence: result.observed_ports,
allocation: alloc_str, allocation: alloc_str,
@@ -2475,7 +2491,7 @@ async fn answer_call(
/// or temporarily unreachable for reflect but the call can still /// or temporarily unreachable for reflect but the call can still
/// proceed with STUN-discovered addresses. /// proceed with STUN-discovered addresses.
async fn try_reflect_own_addr(state: &Arc<AppState>) -> Result<Option<String>, String> { async fn try_reflect_own_addr(state: &Arc<AppState>) -> Result<Option<String>, String> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
let (tx, rx) = tokio::sync::oneshot::channel::<std::net::SocketAddr>(); let (tx, rx) = tokio::sync::oneshot::channel::<std::net::SocketAddr>();
let transport = { let transport = {
let mut sig = state.signal.lock().await; let mut sig = state.signal.lock().await;
@@ -2562,7 +2578,7 @@ async fn try_stun_fallback(state: &Arc<AppState>) -> Result<Option<String>, Stri
/// with `new URL(...)` / a regex if needed. /// with `new URL(...)` / a regex if needed.
#[tauri::command] #[tauri::command]
async fn get_reflected_address(state: tauri::State<'_, Arc<AppState>>) -> Result<String, String> { async fn get_reflected_address(state: tauri::State<'_, Arc<AppState>>) -> Result<String, String> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
let (tx, rx) = tokio::sync::oneshot::channel::<std::net::SocketAddr>(); let (tx, rx) = tokio::sync::oneshot::channel::<std::net::SocketAddr>();
let transport = { let transport = {
let mut sig = state.signal.lock().await; let mut sig = state.signal.lock().await;
@@ -2766,7 +2782,7 @@ async fn hangup_call(
state: tauri::State<'_, Arc<AppState>>, state: tauri::State<'_, Arc<AppState>>,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<(), String> { ) -> Result<(), String> {
use wzp_proto::SignalMessage; use wzp_proto::{SignalMessage, default_signal_version};
emit_call_debug(&app, "hangup_call:start", serde_json::json!({})); emit_call_debug(&app, "hangup_call:start", serde_json::json!({}));
@@ -2778,6 +2794,7 @@ async fn hangup_call(
if let Some(ref transport) = sig.transport { if let Some(ref transport) = sig.transport {
match transport match transport
.send_signal(&SignalMessage::Hangup { .send_signal(&SignalMessage::Hangup {
version: default_signal_version(),
reason: wzp_proto::HangupReason::Normal, reason: wzp_proto::HangupReason::Normal,
call_id: None, call_id: None,
}) })

View File

@@ -1321,7 +1321,7 @@ Statuses (in order of progression):
| T2.6 | Approved | Kimi Code CLI | 2026-05-11T17:45Z | 2026-05-11T17:55Z | [report](reports/T2.6-report.md) | Substance good (Prom metrics); bundled in 54c1a35. Consolidated reviewer notes here. | | T2.6 | Approved | Kimi Code CLI | 2026-05-11T17:45Z | 2026-05-11T17:55Z | [report](reports/T2.6-report.md) | Substance good (Prom metrics); bundled in 54c1a35. Consolidated reviewer notes here. |
| T3.1 | Approved | Kimi Code CLI | 2026-05-11T20:55Z | 2026-05-11T21:05Z | [report](reports/T3.1-report.md) | Approved. DashMap<String, Arc<RwLock<Room>>>; W13 resolved. One commit per task this time — good. Two minor process notes in report. | | T3.1 | Approved | Kimi Code CLI | 2026-05-11T20:55Z | 2026-05-11T21:05Z | [report](reports/T3.1-report.md) | Approved. DashMap<String, Arc<RwLock<Room>>>; W13 resolved. One commit per task this time — good. Two minor process notes in report. |
| T3.2 | Committed | Kimi Code CLI | 2026-05-11T21:15Z | 2026-05-11T21:25Z | [report](reports/T3.2-report.md) | timestamp_ms monotonic across rekey; doc + test. | | T3.2 | Committed | Kimi Code CLI | 2026-05-11T21:15Z | 2026-05-11T21:25Z | [report](reports/T3.2-report.md) | timestamp_ms monotonic across rekey; doc + test. |
| T3.3 | Open | — | — | — | — | — | | T3.3 | Pending Review | Kimi Code CLI | 2026-05-11T16:29Z | 2026-05-11T16:29Z | [report](reports/T3.3-report.md) | — |
| T3.4 | Open | — | — | — | — | — | | T3.4 | Open | — | — | — | — | — |
| T3.5 | Open | — | — | — | — | — | | T3.5 | Open | — | — | — | — | — |
| T4.1 | Open | — | — | — | — | Skeleton — expand before claiming | | T4.1 | Open | — | — | — | — | Skeleton — expand before claiming |
@@ -1350,5 +1350,6 @@ Items currently waiting on the reviewer:
- T1.8 — Per-stream anti-replay window with configurable size — report: reports/T1.8-report.md - T1.8 — Per-stream anti-replay window with configurable size — report: reports/T1.8-report.md
- T2.1 — Add `SignalMessage::TransportFeedback` — report: reports/T2.1-report.md - T2.1 — Add `SignalMessage::TransportFeedback` — report: reports/T2.1-report.md
- T2.2 — `BandwidthEstimator` in `wzp-proto::bandwidth` — report: reports/T2.2-report.md - T2.2 — `BandwidthEstimator` in `wzp-proto::bandwidth` — report: reports/T2.2-report.md
- T3.3 — SignalMessage version field — report: reports/T3.3-report.md
Once a task moves to `Pending Review`, add a line here so the reviewer sees it: `- T<id> — <one-line summary> — report: reports/T<id>-report.md`. The reviewer removes the line when they mark it `Approved` (or moves it back to the agent on `Changes Requested`). Once a task moves to `Pending Review`, add a line here so the reviewer sees it: `- T<id> — <one-line summary> — report: reports/T<id>-report.md`. The reviewer removes the line when they mark it `Approved` (or moves it back to the agent on `Changes Requested`).

View File

@@ -0,0 +1,100 @@
# T3.3 — SignalMessage version field (W12)
**Status:** Pending Review
**Agent:** Kimi Code CLI
**Started:** 2026-05-11T16:29Z
**Completed:** 2026-05-11T16:29Z
**Commit:** (see git log)
**PRD:** ../PRD-protocol-hardening.md
## What I changed
- `crates/wzp-proto/src/packet.rs:540-551` — Added rustdoc explaining `#[serde(other)]` feasibility research and version-field semantics.
- `crates/wzp-proto/src/packet.rs:556-1209` — Added `#[serde(default = "default_signal_version")] version: u8` as the first field to all 38 non-unit `SignalMessage` variants.
- `crates/wzp-proto/src/packet.rs:1217-1220` — Added `pub fn default_signal_version() -> u8 { 1 }`.
- `crates/wzp-proto/src/packet.rs:2590-2669` — Added backward-compat tests: `old_payload_without_version_deserializes` and `new_payload_with_version_deserializes`.
- `crates/wzp-proto/src/lib.rs:32-37` — Re-exported `default_signal_version`.
- `crates/wzp-client/src/handshake.rs`, `crates/wzp-client/src/cli.rs`, `crates/wzp-client/src/ice_agent.rs`, `crates/wzp-client/src/reflect.rs`, `crates/wzp-client/src/analyzer.rs`, `crates/wzp-client/src/featherchat.rs`, `crates/wzp-client/tests/handshake_integration.rs` — Updated constructors and patterns for `SignalMessage` variants to include `version` field.
- `crates/wzp-relay/src/main.rs`, `crates/wzp-relay/src/federation.rs`, `crates/wzp-relay/src/handshake.rs`, `crates/wzp-relay/src/probe.rs`, `crates/wzp-relay/src/relay_link.rs`, `crates/wzp-relay/src/room.rs`, `crates/wzp-relay/src/route.rs`, `crates/wzp-relay/src/signal_hub.rs` — Updated constructors and patterns for `SignalMessage` variants.
- `crates/wzp-relay/tests/cross_relay_direct_call.rs`, `crates/wzp-relay/tests/federation.rs`, `crates/wzp-relay/tests/handshake_integration.rs`, `crates/wzp-relay/tests/hole_punching.rs`, `crates/wzp-relay/tests/multi_reflect.rs`, `crates/wzp-relay/tests/reflect.rs` — Updated test constructors and patterns.
- `crates/wzp-android/src/engine.rs` — Updated constructors and patterns.
- `crates/wzp-web/src/main.rs` — Updated import ordering (cargo fmt).
- `crates/wzp-crypto/tests/featherchat_compat.rs` — Updated import ordering (cargo fmt).
- `desktop/src-tauri/src/engine.rs`, `desktop/src-tauri/src/lib.rs` — Updated patterns and constructors.
## Why these choices
- Used `#[serde(default = "default_signal_version")]` instead of plain `#[serde(default)]` because the spec explicitly required a named helper `fn default_signal_version() -> u8 { 1 }`. The explicit function is also clearer for readers and makes the default value discoverable via rustdoc.
- Unit variants (`Hold`, `Unhold`, `Mute`, `Unmute`, `Reflect`, `TransferAck`) were intentionally left without a `version` field because they carry no struct fields to attach metadata to. Adding a phantom `version` to a unit variant would change its JSON representation from `"Hold"` to `{"Hold": {"version": 1}}`, which is a wire-format break.
- The `Unknown` variant with `#[serde(other)]` was researched and skipped per the spec's own fallback instruction: `#[serde(other)]` only works for internally/externally tagged enums where the tag is a string or integer value. With externally tagged representation (Rust's default), the variant name IS the tag, so there is no "other" value to catch. `bincode` also does not support `#[serde(other)]`. This limitation is documented in the `SignalMessage` rustdoc.
- Removed the unused `is_default_version` helper that the previous session had added; it was dead code after `skip_serializing_if` was dropped (bincode does not support `skip_serializing_if`).
## Deviations from the task spec
- **Step 2:** Did not add `#[serde(other)] Unknown` variant. The spec explicitly allows skipping this if "not feasible" after research. Research confirmed it is not feasible with externally tagged enums + bincode. The limitation is documented in the `SignalMessage` rustdoc.
- **Step 3:** No decode-path warning for `Unknown` because the `Unknown` variant does not exist. Unknown variants naturally produce a serde deserialization error, which is the correct behavior for the signal protocol.
## Verification output
```
$ cargo test -p wzp-proto --lib
running 121 tests
...
test result: ok. 121 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s
```
```
$ cargo test -p wzp-proto -- transport_feedback
running 2 tests
test packet::tests::transport_feedback_default_version ... ok
test packet::tests::transport_feedback_roundtrip ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 119 filtered out; finished in 0.00s
```
```
$ cargo test -p wzp-proto -- old_payload
running 1 test
test packet::tests::old_payload_without_version_deserializes ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 120 filtered out; finished in 0.00s
```
```
$ cargo test -p wzp-proto -- new_payload
running 1 test
test packet::tests::new_payload_with_version_deserializes ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 120 filtered out; finished in 0.00s
```
```
$ cargo test --workspace --exclude wzp-android --no-fail-fast
... (all crates pass)
Total: 610 passed; 0 failed
```
## Test summary
- Tests added: 2
- `old_payload_without_version_deserializes` — proves old `CallOffer`, `Ping`, and `Hangup` JSON without `version` deserialize with default `1`
- `new_payload_with_version_deserializes` — proves explicit `version: 2` in JSON is preserved on deserialize
- Tests modified: 1
- `transport_feedback_default_version` — updated expected version from `0` to `1` to match new default semantic
- Workspace test count before: ~571 (per TASKS.md env setup) / after: 610
- `cargo clippy --workspace --all-targets -- -D warnings`: fails in pre-existing debt only (`warzone-protocol` 3 errors, `wzp-codec` 9 errors; see PROTOCOL-AUDIT.md). Crate touched by this task (`wzp-proto`) is clean.
- `cargo fmt --all -- --check`: pass
## Risks / follow-ups
- **T3.2 status corruption:** The status board shows T3.2 as `Committed`, which is not a valid workflow status. Per the agent instructions, I did not touch already-reviewed tasks. The reviewer should flip T3.2 to `Approved` (its actual status from prior review).
- Unit variants (`Hold`, `Unhold`, `Mute`, `Unmute`, `Reflect`, `TransferAck`) have no `version` field. If future protocol evolution requires versioning these, they will need to be converted to struct variants, which is a wire-format change.
- The `cargo test -p wzp-proto signal_message` filter pattern from the task spec matches 0 tests because no test names contain "signal_message". The actual tests (`transport_feedback_default_version`, `old_payload_without_version_deserializes`, `new_payload_with_version_deserializes`) verify the behavior.
## Reviewer checklist (filled in by reviewer)
- [ ] Code matches PRD intent
- [ ] Verification output is real (re-run if suspicious)
- [ ] No backward-incompat surprises
- [ ] Tests cover the new behavior
- [ ] Approved