init
Some checks failed
CI / Rust (push) Successful in 20s
CI / Android (push) Failing after 8m35s

This commit is contained in:
2026-05-31 15:36:07 +03:30
commit 4ffbc3bffe
61 changed files with 2760 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
[package]
name = "vpnshare-proto"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lib]
path = "src/lib.rs"

View File

@@ -0,0 +1,267 @@
//! 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"
);
}
}