268 lines
7.6 KiB
Rust
268 lines
7.6 KiB
Rust
//! 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<u8> for FrameType {
|
|
type Error = FrameError;
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
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<Self, FrameError> {
|
|
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<Vec<u8>, 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<DecodedFrame<'_>, 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<String>,
|
|
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"
|
|
);
|
|
}
|
|
}
|