clone

one developer profile for links and private tcp services, reached peer to peer over iroh with no open ports

main
devsite / crates / devsite-proto / src / wire.rs
wire.rs 4.4K
1//! Frames exchanged over an Iroh bidirectional stream, and their codec.2//!3//! A stream begins with one request frame and one response frame. After `Connected`, the4//! rest of the Iroh stream is the service's uninterpreted byte stream.56use serde::{Deserialize, Serialize};78use crate::capability::SignedCapability;910/// Hard ceiling on any single frame. Bounds daemon memory against a hostile peer: the11/// length prefix is attacker-controlled, so it is checked *before* allocating.12pub const MAX_FRAME_BYTES: usize = 4 * 1024 * 1024;1314#[derive(Debug, Clone, Serialize, Deserialize)]15pub enum Request {16 Connect(ConnectRequest),17}1819#[derive(Debug, Clone, Serialize, Deserialize)]20pub struct ConnectRequest {21 /// The grant authorizing this connection. Required, not optional: a daemon must never22 /// have a code path where an absent capability means "allow".23 pub capability: SignedCapability,24}2526#[derive(Debug, Clone, Serialize, Deserialize)]27pub enum Response {28 Connected,29 Error { code: ErrorCode, message: String },30}3132/// Deliberately coarse. A peer that fails authorization learns only "denied" — never33/// whether the resource existed, whether the signature or the binding was the problem, or34/// anything else that would let it probe the daemon's configuration.35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]36pub enum ErrorCode {37 Denied,38 BadRequest,39 UpstreamUnavailable,40}4142#[derive(Debug, thiserror::Error)]43pub enum CodecError {44 #[error("frame of {size} bytes exceeds the {MAX_FRAME_BYTES} byte limit")]45 TooLarge { size: usize },46 #[error("malformed frame: {0}")]47 Malformed(#[from] postcard::Error),48}4950/// Encode a frame with its length prefix. Fails rather than emitting an oversized frame,51/// so a bug on our side can't produce something the peer will reject as hostile.52pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {53 let payload = postcard::to_allocvec(value)?;54 if payload.len() > MAX_FRAME_BYTES {55 return Err(CodecError::TooLarge {56 size: payload.len(),57 });58 }59 let mut out = Vec::with_capacity(payload.len() + 4);60 out.extend_from_slice(&(payload.len() as u32).to_le_bytes());61 out.extend_from_slice(&payload);62 Ok(out)63}6465/// Validate a length prefix before it is used to size an allocation.66pub fn frame_len(prefix: [u8; 4]) -> Result<usize, CodecError> {67 let size = u32::from_le_bytes(prefix) as usize;68 if size > MAX_FRAME_BYTES {69 return Err(CodecError::TooLarge { size });70 }71 Ok(size)72}7374pub fn decode<T: for<'de> Deserialize<'de>>(payload: &[u8]) -> Result<T, CodecError> {75 Ok(postcard::from_bytes(payload)?)76}7778#[cfg(test)]79mod tests {80 use super::*;8182 fn sample_capability() -> SignedCapability {83 use crate::capability::{CapabilityClaims, Permission};84 use crate::{AccountId, ResourceId};8586 let key = ed25519_dalek::SigningKey::from_bytes(&[5; 32]);87 let claims = CapabilityClaims {88 issuer: "https://dev.site".to_string(),89 viewer: AccountId::from_bytes([1; 16]),90 resource: ResourceId::from_bytes([2; 16]),91 audience: [7; 32],92 client_key: [8; 32],93 permission: Permission::TcpConnect,94 issued_at: 0,95 expires_at: 300,96 nonce: [3; 16],97 };98 SignedCapability::sign(&claims, &key).unwrap()99 }100101 #[test]102 fn frames_round_trip() {103 let request = Request::Connect(ConnectRequest {104 capability: sample_capability(),105 });106 let encoded = encode(&request).unwrap();107 let size = frame_len(encoded[..4].try_into().unwrap()).unwrap();108 assert_eq!(size, encoded.len() - 4);109110 let decoded: Request = decode(&encoded[4..]).unwrap();111 let Request::Connect(connect) = decoded;112 assert_eq!(113 connect.capability.to_bytes(),114 sample_capability().to_bytes()115 );116 }117118 #[test]119 fn oversized_length_prefix_is_rejected_before_allocating() {120 let prefix = (MAX_FRAME_BYTES as u32 + 1).to_le_bytes();121 assert!(matches!(122 frame_len(prefix),123 Err(CodecError::TooLarge { .. })124 ));125 // The pathological case: a peer claiming ~4 GiB must not cause a 4 GiB allocation.126 assert!(matches!(127 frame_len(u32::MAX.to_le_bytes()),128 Err(CodecError::TooLarge { .. })129 ));130 }131}