Skip to main content

rift_protocol/
lib.rs

1//! Rift Protocol: versioned, framed on-the-wire messages for P2P voice + text.
2//!
3//! A Rift frame is:
4//! - magic: 4 bytes ("RFT1")
5//! - version: u8
6//! - frame_len: u32 (length of the encoded frame body)
7//! - frame body: bincode-encoded `(RiftFrameHeader, RiftPayload)`
8//!
9//! Versioning: each peer advertises supported protocol versions. The highest
10//! common version is selected for communication.
11//!
12//! Streams: frames declare a `StreamKind` (Control / Text / Voice / Custom)
13//! to allow multiplexing and future extensions.
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18pub use rift_core::{ChannelId, MessageId, PeerId};
19use rand::rngs::OsRng;
20use rand::RngCore;
21
22const MAGIC: &[u8; 4] = b"RFT1";
23const MAX_FRAME_LEN: usize = 64 * 1024;
24
25/// On-the-wire protocol versions.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
27#[repr(u8)]
28pub enum ProtocolVersion {
29    V1 = 1,
30    V2 = 2,
31}
32
33impl ProtocolVersion {
34    /// Convert to the raw u8 that is encoded on the wire.
35    pub fn as_u8(self) -> u8 {
36        self as u8
37    }
38
39    /// Parse from a raw u8, returning None if unsupported.
40    pub fn from_u8(value: u8) -> Option<Self> {
41        match value {
42            1 => Some(ProtocolVersion::V1),
43            2 => Some(ProtocolVersion::V2),
44            _ => None,
45        }
46    }
47}
48
49/// Logical stream classification used to multiplex payloads.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum StreamKind {
52    Voice,
53    Text,
54    Control,
55    Custom(u16),
56}
57
58/// Audio codec identifiers used for voice frames.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60pub enum CodecId {
61    Opus,
62    PCM16,
63    Experimental(u16),
64}
65
66/// Feature flags advertised during capability exchange.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub enum FeatureFlag {
69    Voice,
70    Text,
71    Relay,
72    E2EE,
73    ScreenShare,
74    DataChannel,
75}
76
77/// Session identifier derived from channel metadata.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub struct SessionId(pub [u8; 32]);
80
81impl SessionId {
82    /// An all-zero sentinel session id (unused for real sessions).
83    pub const NONE: SessionId = SessionId([0u8; 32]);
84
85    /// Generate a random session id.
86    pub fn random() -> Self {
87        let mut bytes = [0u8; 32];
88        OsRng.fill_bytes(&mut bytes);
89        SessionId(bytes)
90    }
91
92    /// Derive a deterministic session id from channel name + optional password.
93    pub fn from_channel(name: &str, password: Option<&str>) -> Self {
94        let mut hasher = blake3::Hasher::new();
95        hasher.update(b"rift-channel:");
96        hasher.update(name.as_bytes());
97        if let Some(password) = password {
98            hasher.update(b":");
99            hasher.update(password.as_bytes());
100        }
101        let hash = hasher.finalize();
102        let mut bytes = [0u8; 32];
103        bytes.copy_from_slice(hash.as_bytes());
104        SessionId(bytes)
105    }
106
107    /// Convert to hex for logs and UI.
108    pub fn to_hex(&self) -> String {
109        hex::encode(self.0)
110    }
111}
112
113/// Header for every framed message.
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct RiftFrameHeader {
116    /// Negotiated protocol version for this session.
117    pub version: ProtocolVersion,
118    /// Stream classification (text, voice, control, etc).
119    pub stream: StreamKind,
120    /// Custom flags for future expansions.
121    pub flags: u16,
122    /// Monotonic sequence number for the sender.
123    pub seq: u32,
124    /// Sender timestamp in milliseconds.
125    pub timestamp: u64,
126    /// Sender peer id.
127    pub source: PeerId,
128    /// Session id the frame belongs to.
129    pub session: SessionId,
130}
131
132/// High-level chat message payload.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ChatMessage {
135    /// Content-addressed message id.
136    pub id: MessageId,
137    /// Sender peer id.
138    pub from: PeerId,
139    /// Sender timestamp.
140    pub timestamp: u64,
141    /// Raw message text.
142    pub text: String,
143}
144
145impl ChatMessage {
146    /// Construct a new message and compute its id.
147    pub fn new(from: PeerId, timestamp: u64, text: String) -> Self {
148        let id = MessageId::new(from, timestamp, &text);
149        Self {
150            id,
151            from,
152            timestamp,
153            text,
154        }
155    }
156}
157
158/// Capability advertisement payload for negotiation.
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct Capabilities {
161    /// Supported protocol versions in preference order.
162    pub supported_versions: Vec<ProtocolVersion>,
163    /// Supported audio codecs.
164    pub audio_codecs: Vec<CodecId>,
165    /// Feature flags (voice, text, E2EE, relay, etc).
166    pub features: Vec<FeatureFlag>,
167    /// Optional bitrate ceiling.
168    pub max_bitrate: Option<u32>,
169    /// Preferred audio frame duration.
170    pub preferred_frame_duration_ms: Option<u16>,
171}
172
173/// Call session lifecycle state.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175pub enum CallState {
176    Ringing,
177    Active,
178    Ended,
179}
180
181/// Group topology mode for multi-peer calls.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
183pub enum GroupMode {
184    Mesh,
185    Hybrid { forwarder: PeerId },
186}
187
188pub type StreamId = u64;
189
190/// Group-level control messages.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub enum GroupControl {
193    Join { session: SessionId, peer_id: PeerId },
194    Leave { session: SessionId, peer_id: PeerId },
195    StreamPublish {
196        session: SessionId,
197        stream_id: StreamId,
198        from: PeerId,
199        codec: CodecId,
200    },
201    StreamSubscribe {
202        session: SessionId,
203        stream_id: StreamId,
204        from: PeerId,
205    },
206    Topology {
207        session: SessionId,
208        mode: GroupMode,
209    },
210}
211
212/// One-to-one call control messages.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub enum CallControl {
215    Invite {
216        session: SessionId,
217        from: PeerId,
218        to: PeerId,
219        display_name: Option<String>,
220        #[serde(default)]
221        rndzv_srt_uri: Option<String>,
222    },
223    Accept { session: SessionId, from: PeerId },
224    Decline {
225        session: SessionId,
226        from: PeerId,
227        reason: Option<String>,
228    },
229    Bye { session: SessionId, from: PeerId },
230    Mute {
231        session: SessionId,
232        from: PeerId,
233        muted: bool,
234    },
235    SessionInfo {
236        session: SessionId,
237        participants: Vec<PeerId>,
238    },
239}
240
241/// ICE-lite candidate classification.
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
243pub enum CandidateType {
244    Host,
245    Srflx,
246    Relay,
247}
248
249/// ICE-lite candidate structure.
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct IceCandidate {
252    /// Socket address for the candidate.
253    pub addr: std::net::SocketAddr,
254    /// Candidate type (host/srflx/relay).
255    pub cand_type: CandidateType,
256    /// Priority used for selection heuristics.
257    pub priority: u32,
258    /// Foundation group for candidate correlation.
259    pub foundation: u64,
260}
261
262/// Control plane messages exchanged over the Control stream.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub enum ControlMessage {
265    Join { peer_id: PeerId, display_name: Option<String> },
266    Hello {
267        peer_id: PeerId,
268        public_key: Vec<u8>,
269        capabilities: Capabilities,
270        #[serde(default)]
271        candidates: Vec<std::net::SocketAddr>,
272    },
273    IceCandidates {
274        peer_id: PeerId,
275        session: SessionId,
276        candidates: Vec<IceCandidate>,
277    },
278    IceCheck {
279        session: SessionId,
280        tie_breaker: u64,
281        candidate: IceCandidate,
282    },
283    IceCheckAck {
284        session: SessionId,
285        candidate: IceCandidate,
286    },
287    KeyInit {
288        session: SessionId,
289        eph_pub_x25519: [u8; 32],
290        sig_ed25519: Vec<u8>,
291    },
292    KeyResp {
293        session: SessionId,
294        eph_pub_x25519: [u8; 32],
295        sig_ed25519: Vec<u8>,
296    },
297    EncryptedReady {
298        session: SessionId,
299        alg: u8,
300    },
301    Leave { peer_id: PeerId },
302    PeerState { peer_id: PeerId, relay_capable: bool },
303    Chat(ChatMessage),
304    Ping { nonce: u64, sent_at_ms: u64 },
305    Pong { nonce: u64, sent_at_ms: u64 },
306    Auth { token: Vec<u8> },
307    RouteInfo { from: PeerId, to: PeerId, relayed: bool },
308    Capabilities(Capabilities),
309    CapabilitiesUpdate(Capabilities),
310    PeerList { peers: Vec<PeerInfo> },
311    Call(CallControl),
312    Group(GroupControl),
313    E2eeInit {
314        session: SessionId,
315        from: PeerId,
316        public_key: [u8; 32],
317        signature: Vec<u8>,
318    },
319    E2eeResp {
320        session: SessionId,
321        from: PeerId,
322        public_key: [u8; 32],
323        signature: Vec<u8>,
324    },
325}
326
327/// Encrypted payload wrapper for E2EE.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct EncryptedPayload {
330    /// AEAD nonce (12 bytes for AES-GCM).
331    pub nonce: [u8; 12],
332    /// Ciphertext bytes.
333    pub ciphertext: Vec<u8>,
334}
335
336/// Peer metadata used in peer lists and routing.
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct PeerInfo {
339    /// Peer id.
340    pub peer_id: PeerId,
341    /// Primary address.
342    pub addr: std::net::SocketAddr,
343    /// Optional additional addresses.
344    #[serde(default)]
345    pub addrs: Vec<std::net::SocketAddr>,
346    /// Relay capability flag.
347    pub relay_capable: bool,
348}
349
350/// Voice packet payload (pre-Opus or Opus-encoded audio data).
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct VoicePacket {
353    /// Codec for this packet.
354    pub codec_id: CodecId,
355    /// Encoded audio payload.
356    pub payload: Vec<u8>,
357}
358
359/// QoS profile for adaptive audio tuning.
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct QosProfile {
362    /// Target end-to-end latency.
363    pub target_latency_ms: u32,
364    /// Maximum tolerated latency.
365    pub max_latency_ms: u32,
366    /// Minimum bitrate to preserve intelligibility.
367    pub min_bitrate: u32,
368    /// Maximum bitrate to avoid saturation.
369    pub max_bitrate: u32,
370    /// Packet loss tolerance threshold.
371    pub packet_loss_tolerance: f32,
372}
373
374impl Default for QosProfile {
375    fn default() -> Self {
376        Self {
377            target_latency_ms: 50,
378            max_latency_ms: 200,
379            min_bitrate: 16_000,
380            max_bitrate: 96_000,
381            packet_loss_tolerance: 0.08,
382        }
383    }
384}
385
386/// The union of all possible payloads for a Rift frame.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub enum RiftPayload {
389    Control(ControlMessage),
390    Voice(VoicePacket),
391    Text(ChatMessage),
392    Relay { target: PeerId, inner: Box<RiftPayload> },
393    Encrypted(EncryptedPayload),
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399
400    #[test]
401    fn group_control_roundtrip() {
402        let session = SessionId::random();
403        let msg = ControlMessage::Group(GroupControl::Topology {
404            session,
405            mode: GroupMode::Hybrid {
406                forwarder: PeerId([7u8; 32]),
407            },
408        });
409        let bytes = bincode::serialize(&msg).expect("serialize");
410        let decoded: ControlMessage = bincode::deserialize(&bytes).expect("deserialize");
411        match decoded {
412            ControlMessage::Group(GroupControl::Topology { session: s, mode }) => {
413                assert_eq!(s, session);
414                assert!(matches!(mode, GroupMode::Hybrid { .. }));
415            }
416            other => panic!("unexpected: {other:?}"),
417        }
418    }
419
420    #[test]
421    fn decode_rejects_oversize_frames() {
422        let mut bytes = Vec::new();
423        bytes.extend_from_slice(MAGIC);
424        bytes.push(ProtocolVersion::V1.as_u8());
425        let len = (MAX_FRAME_LEN + 1) as u32;
426        bytes.extend_from_slice(&len.to_le_bytes());
427        bytes.resize(10, 0);
428        let res = decode_frame(&bytes);
429        assert!(matches!(res, Err(FrameError::FrameTooLarge)));
430    }
431}
432
433/// Errors encountered when decoding protocol frames.
434#[derive(Debug, Error)]
435pub enum FrameError {
436    #[error("invalid magic")]
437    InvalidMagic,
438    #[error("unsupported version {0}")]
439    UnsupportedVersion(u8),
440    #[error("frame length mismatch")]
441    LengthMismatch,
442    #[error("frame too large")]
443    FrameTooLarge,
444    #[error("decode error: {0}")]
445    Decode(#[from] bincode::Error),
446}
447
448/// Return the protocol versions supported by this build.
449pub fn supported_versions() -> &'static [ProtocolVersion] {
450    &[ProtocolVersion::V2, ProtocolVersion::V1]
451}
452
453/// Select the highest mutual version between two peers.
454pub fn select_version(theirs: &[ProtocolVersion]) -> Option<ProtocolVersion> {
455    let mut ours = supported_versions().to_vec();
456    ours.sort();
457    let mut theirs = theirs.to_vec();
458    theirs.sort();
459    ours.into_iter()
460        .rev()
461        .find(|v| theirs.contains(v))
462}
463
464/// Encode a framed message into bytes:
465/// magic + version + length + bincode(body).
466pub fn encode_frame(header: &RiftFrameHeader, payload: &RiftPayload) -> Vec<u8> {
467    let body = bincode::serialize(&(header, payload)).expect("serialize frame");
468    let mut out = Vec::with_capacity(4 + 1 + 4 + body.len());
469    out.extend_from_slice(MAGIC);
470    out.push(header.version.as_u8());
471    out.extend_from_slice(&(body.len() as u32).to_le_bytes());
472    out.extend_from_slice(&body);
473    out
474}
475
476/// Decode a framed message from bytes, validating length and magic.
477pub fn decode_frame(bytes: &[u8]) -> Result<(RiftFrameHeader, RiftPayload), FrameError> {
478    if bytes.len() < 9 {
479        return Err(FrameError::LengthMismatch);
480    }
481    if &bytes[..4] != MAGIC {
482        return Err(FrameError::InvalidMagic);
483    }
484    let version = ProtocolVersion::from_u8(bytes[4]).ok_or(FrameError::UnsupportedVersion(bytes[4]))?;
485    let len = u32::from_le_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]) as usize;
486    if len > MAX_FRAME_LEN {
487        return Err(FrameError::FrameTooLarge);
488    }
489    if bytes.len() < 9 + len {
490        return Err(FrameError::LengthMismatch);
491    }
492    let body = &bytes[9..9 + len];
493    let (mut header, payload): (RiftFrameHeader, RiftPayload) = bincode::deserialize(body)?;
494    header.version = version;
495    Ok((header, payload))
496}