//! VSHP protocol primitives. //! //! This crate intentionally has no external dependencies. Cryptographic //! handshake implementation will be linked behind a reviewed crypto boundary; //! this module owns stable frame layout and parser behavior. use core::fmt; pub const MAGIC: [u8; 4] = *b"VSHP"; pub const PROTOCOL_VERSION: u8 = 1; pub const HEADER_LEN: usize = 24; pub const MAX_PAYLOAD_LEN: usize = 1_048_576; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum FrameType { Hello = 1, Auth = 2, Config = 3, IpPacket = 4, Ping = 5, Resume = 6, Stats = 7, Close = 8, } impl TryFrom for FrameType { type Error = FrameError; fn try_from(value: u8) -> Result { match value { 1 => Ok(Self::Hello), 2 => Ok(Self::Auth), 3 => Ok(Self::Config), 4 => Ok(Self::IpPacket), 5 => Ok(Self::Ping), 6 => Ok(Self::Resume), 7 => Ok(Self::Stats), 8 => Ok(Self::Close), other => Err(FrameError::UnknownFrameType(other)), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FrameHeader { pub version: u8, pub frame_type: FrameType, pub flags: u16, pub stream_id: u32, pub packet_number: u64, pub payload_len: u32, } impl FrameHeader { pub fn new( frame_type: FrameType, stream_id: u32, packet_number: u64, payload_len: usize, ) -> Result { if payload_len > MAX_PAYLOAD_LEN { return Err(FrameError::PayloadTooLarge(payload_len)); } Ok(Self { version: PROTOCOL_VERSION, frame_type, flags: 0, stream_id, packet_number, payload_len: payload_len as u32, }) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct DecodedFrame<'a> { pub header: FrameHeader, pub payload: &'a [u8], pub consumed: usize, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum FrameError { Incomplete, BadMagic([u8; 4]), UnsupportedVersion(u8), UnknownFrameType(u8), PayloadTooLarge(usize), LengthMismatch { declared: usize, available: usize }, } impl fmt::Display for FrameError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Incomplete => write!(f, "incomplete frame"), Self::BadMagic(magic) => write!(f, "bad frame magic: {magic:?}"), Self::UnsupportedVersion(version) => write!(f, "unsupported protocol version: {version}"), Self::UnknownFrameType(frame_type) => write!(f, "unknown frame type: {frame_type}"), Self::PayloadTooLarge(length) => write!(f, "payload too large: {length}"), Self::LengthMismatch { declared, available } => { write!(f, "payload length mismatch: declared {declared}, available {available}") } } } } impl std::error::Error for FrameError {} pub fn encode_frame( frame_type: FrameType, stream_id: u32, packet_number: u64, payload: &[u8], ) -> Result, FrameError> { let header = FrameHeader::new(frame_type, stream_id, packet_number, payload.len())?; let mut out = Vec::with_capacity(HEADER_LEN + payload.len()); out.extend_from_slice(&MAGIC); out.push(header.version); out.push(header.frame_type as u8); out.extend_from_slice(&header.flags.to_be_bytes()); out.extend_from_slice(&header.stream_id.to_be_bytes()); out.extend_from_slice(&header.packet_number.to_be_bytes()); out.extend_from_slice(&header.payload_len.to_be_bytes()); out.extend_from_slice(payload); Ok(out) } pub fn decode_frame(input: &[u8]) -> Result, FrameError> { if input.len() < HEADER_LEN { return Err(FrameError::Incomplete); } let magic = [input[0], input[1], input[2], input[3]]; if magic != MAGIC { return Err(FrameError::BadMagic(magic)); } let version = input[4]; if version != PROTOCOL_VERSION { return Err(FrameError::UnsupportedVersion(version)); } let frame_type = FrameType::try_from(input[5])?; let flags = u16::from_be_bytes([input[6], input[7]]); let stream_id = u32::from_be_bytes([input[8], input[9], input[10], input[11]]); let packet_number = u64::from_be_bytes([ input[12], input[13], input[14], input[15], input[16], input[17], input[18], input[19], ]); let payload_len = u32::from_be_bytes([input[20], input[21], input[22], input[23]]) as usize; if payload_len > MAX_PAYLOAD_LEN { return Err(FrameError::PayloadTooLarge(payload_len)); } let available = input.len().saturating_sub(HEADER_LEN); if available < payload_len { return Err(FrameError::LengthMismatch { declared: payload_len, available, }); } let consumed = HEADER_LEN + payload_len; Ok(DecodedFrame { header: FrameHeader { version, frame_type, flags, stream_id, packet_number, payload_len: payload_len as u32, }, payload: &input[HEADER_LEN..consumed], consumed, }) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CloseCode { Normal = 0, UnsupportedVersion = 1, AuthenticationFailed = 2, PeerRevoked = 3, VpnUnavailable = 4, PolicyDenied = 5, ProtocolError = 6, Overload = 7, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct PairingTicket { pub session_id: String, pub server_public_key: String, pub one_time_psk: String, pub transports: Vec, pub expires_unix_seconds: u64, } impl PairingTicket { pub fn to_uri(&self) -> String { format!( "vshare://pair?v=1&sid={}&server_pk={}&psk={}&transports={}&expires={}", self.session_id, self.server_public_key, self.one_time_psk, self.transports.join(","), self.expires_unix_seconds ) } } #[cfg(test)] mod tests { use super::*; #[test] fn frame_round_trips() { let payload = b"hello"; let encoded = encode_frame(FrameType::Hello, 42, 7, payload).unwrap(); let decoded = decode_frame(&encoded).unwrap(); assert_eq!(decoded.header.frame_type, FrameType::Hello); assert_eq!(decoded.header.stream_id, 42); assert_eq!(decoded.header.packet_number, 7); assert_eq!(decoded.payload, payload); assert_eq!(decoded.consumed, HEADER_LEN + payload.len()); } #[test] fn rejects_bad_magic() { let mut encoded = encode_frame(FrameType::Ping, 0, 1, b"").unwrap(); encoded[0] = b'X'; assert!(matches!(decode_frame(&encoded), Err(FrameError::BadMagic(_)))); } #[test] fn rejects_incomplete_payload() { let encoded = encode_frame(FrameType::Stats, 0, 1, b"abcdef").unwrap(); let truncated = &encoded[..encoded.len() - 2]; assert!(matches!( decode_frame(truncated), Err(FrameError::LengthMismatch { declared: 6, available: 4 }) )); } #[test] fn encodes_pairing_uri() { let ticket = PairingTicket { session_id: "s".into(), server_public_key: "k".into(), one_time_psk: "p".into(), transports: vec!["usb".into(), "wifi".into()], expires_unix_seconds: 123, }; assert_eq!( ticket.to_uri(), "vshare://pair?v=1&sid=s&server_pk=k&psk=p&transports=usb,wifi&expires=123" ); } }