//! Embeddable reliable stream machinery for Dosh-native application transports. //! //! The terminal client/server binaries use the same wire packets exposed here: //! `StreamOpen`, `StreamData`, `StreamWindowAdjust`, and `StreamClose`. This //! module keeps the stream state machine independent from UDP sockets, native //! auth, terminal rendering, or any particular service protocol. An embedding //! application is expected to: //! //! 1. authenticate and maintain a Dosh session using [`crate::protocol`], //! 2. encrypt each [`OutgoingStreamPacket::body`] as the given packet kind, //! 3. pass received stream packet bodies back into [`StreamMux`], //! 4. call [`StreamMux::tick`] periodically to retransmit lost stream data. //! //! That split is intentional: Dosh roaming/reconnect stays at the authenticated //! session layer, while this module provides the reusable fast persistent byte //! streams that higher-level app protocols can embed. use crate::protocol::{ self, CLIENT_TO_SERVER, PacketKind, ReplayWindow, SERVER_TO_CLIENT, StreamClose, StreamData, StreamOpen, StreamOpenOk, StreamOpenReject, StreamWindowAdjust, }; use anyhow::{Result, anyhow, bail}; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::net::UdpSocket; pub const DEFAULT_INITIAL_WINDOW: usize = 1024 * 1024; pub const DEFAULT_RETRANSMIT_AFTER: Duration = Duration::from_millis(200); pub const DEFAULT_KEEPALIVE_AFTER: Duration = Duration::from_secs(2); pub const SERVICE_TARGET_PREFIX: &str = "@dosh-"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct TransportConfig { pub initial_window: usize, pub retransmit_after: Duration, pub keepalive_after: Duration, } impl Default for TransportConfig { fn default() -> Self { Self { initial_window: DEFAULT_INITIAL_WINDOW, retransmit_after: DEFAULT_RETRANSMIT_AFTER, keepalive_after: DEFAULT_KEEPALIVE_AFTER, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionRole { Client, Server, } impl SessionRole { fn send_direction(self) -> u32 { match self { Self::Client => CLIENT_TO_SERVER, Self::Server => SERVER_TO_CLIENT, } } fn recv_direction(self) -> u32 { match self { Self::Client => SERVER_TO_CLIENT, Self::Server => CLIENT_TO_SERVER, } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SessionTransportConfig { pub role: SessionRole, pub conn_id: [u8; 16], pub session_key: [u8; 32], pub peer_addr: SocketAddr, pub initial_send_seq: u64, pub initial_ack: u64, pub stream: TransportConfig, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SessionEvent { Stream(TransportEvent), Ping, Pong, Ignored, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct OutgoingStreamPacket { pub kind: PacketKind, pub body: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct IncomingStreamData { pub stream_id: u64, pub chunks: Vec>, pub received_offset: u64, pub consumed: usize, pub window_adjust: OutgoingStreamPacket, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct IncomingStreamOpen { pub stream_id: u64, pub target_host: String, pub target_port: u16, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct AcceptedService { pub name: String, pub stream_id: u64, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum TransportEvent { Open(IncomingStreamOpen), OpenOk { stream_id: u64, flushed: Vec, }, OpenReject { stream_id: u64, reason: String, }, Data(IncomingStreamData), WindowAdjust { stream_id: u64, flushed: Vec, }, Close { stream_id: u64, }, } #[derive(Debug, Clone)] struct PendingStreamChunk { offset: u64, bytes: Vec, last_sent: Instant, attempts: u32, } #[derive(Debug, Clone)] struct PendingStreamOpen { target_host: String, target_port: u16, last_sent: Instant, attempts: u32, } #[derive(Debug, Clone)] pub struct StreamMux { config: TransportConfig, opened_streams: HashSet, pending_opens: HashMap, send_credit: HashMap, pending_data: HashMap>>, sent_data: HashMap>, next_send_offset: HashMap, next_recv_offset: HashMap, recv_pending: HashMap>>, } #[derive(Debug, Clone, Default)] pub struct ServiceRegistry { services: HashSet, } impl ServiceRegistry { pub fn new() -> Self { Self::default() } pub fn register(&mut self, name: impl Into) -> Result<()> { let name = name.into(); service_target(&name)?; self.services.insert(name); Ok(()) } pub fn is_registered(&self, name: &str) -> bool { self.services.contains(name) } pub fn match_open(&self, open: &IncomingStreamOpen) -> Result { let Some(name) = service_name_from_target(&open.target_host) else { bail!("stream target {} is not a Dosh service", open.target_host); }; if !self.services.contains(name) { bail!("service {name:?} is not registered"); } if open.target_port != 0 { bail!( "service stream {} used non-zero target port", open.stream_id ); } Ok(AcceptedService { name: name.to_string(), stream_id: open.stream_id, }) } } impl StreamMux { pub fn new(config: TransportConfig) -> Self { Self { config, opened_streams: HashSet::new(), pending_opens: HashMap::new(), send_credit: HashMap::new(), pending_data: HashMap::new(), sent_data: HashMap::new(), next_send_offset: HashMap::new(), next_recv_offset: HashMap::new(), recv_pending: HashMap::new(), } } pub fn open_stream( &mut self, stream_id: u64, target_host: impl Into, target_port: u16, ) -> Result { let target_host = target_host.into(); if self.opened_streams.contains(&stream_id) || self.pending_opens.contains_key(&stream_id) { bail!("stream {stream_id} already exists"); } self.send_credit .insert(stream_id, self.config.initial_window); self.next_send_offset.entry(stream_id).or_insert(0); self.next_recv_offset.entry(stream_id).or_insert(0); self.pending_opens.insert( stream_id, PendingStreamOpen { target_host: target_host.clone(), target_port, last_sent: Instant::now(), attempts: 1, }, ); encode_packet( PacketKind::StreamOpen, &StreamOpen { stream_id, target_host, target_port, }, ) } pub fn accept_open(&mut self, open: StreamOpen) -> Result { self.opened_streams.insert(open.stream_id); self.send_credit .entry(open.stream_id) .or_insert(self.config.initial_window); self.next_send_offset.entry(open.stream_id).or_insert(0); self.next_recv_offset.entry(open.stream_id).or_insert(0); Ok(IncomingStreamOpen { stream_id: open.stream_id, target_host: open.target_host, target_port: open.target_port, }) } pub fn open_ok(stream_id: u64) -> Result { encode_packet(PacketKind::StreamOpenOk, &StreamOpenOk { stream_id }) } pub fn open_reject(stream_id: u64, reason: impl Into) -> Result { encode_packet( PacketKind::StreamOpenReject, &StreamOpenReject { stream_id, reason: reason.into(), }, ) } pub fn handle_open_ok(&mut self, ok: StreamOpenOk) -> Result> { self.pending_opens.remove(&ok.stream_id); self.opened_streams.insert(ok.stream_id); self.send_credit .entry(ok.stream_id) .or_insert(self.config.initial_window); self.next_send_offset.entry(ok.stream_id).or_insert(0); self.next_recv_offset.entry(ok.stream_id).or_insert(0); self.flush_pending_data(ok.stream_id) } pub fn handle_open_reject(&mut self, reject: StreamOpenReject) { self.cleanup_stream(reject.stream_id); } pub fn send_data( &mut self, stream_id: u64, bytes: impl Into>, ) -> Result> { let bytes = bytes.into(); if bytes.is_empty() { return Ok(Vec::new()); } if !self.opened_streams.contains(&stream_id) || self.send_credit.get(&stream_id).copied().unwrap_or(0) < bytes.len() || self .pending_data .get(&stream_id) .is_some_and(|pending| !pending.is_empty()) { self.pending_data .entry(stream_id) .or_default() .push_back(bytes); return Ok(Vec::new()); } Ok(vec![self.send_data_now(stream_id, bytes)?]) } pub fn handle_data(&mut self, data: StreamData) -> Result { let stream_id = data.stream_id; let (chunks, consumed, received_offset) = self.accept_data(data); let window_adjust = encode_packet( PacketKind::StreamWindowAdjust, &StreamWindowAdjust { stream_id, received_offset, bytes: consumed.min(u32::MAX as usize) as u32, }, )?; Ok(IncomingStreamData { stream_id, chunks, consumed, received_offset, window_adjust, }) } pub fn handle_window_adjust( &mut self, adjust: StreamWindowAdjust, ) -> Result> { self.ack_data(adjust.stream_id, adjust.received_offset); self.add_credit(adjust.stream_id, adjust.bytes as usize); self.flush_pending_data(adjust.stream_id) } pub fn close_stream(&mut self, stream_id: u64) -> Result { self.cleanup_stream(stream_id); encode_packet(PacketKind::StreamClose, &StreamClose { stream_id }) } pub fn handle_close(&mut self, close: StreamClose) { self.cleanup_stream(close.stream_id); } pub fn handle_packet(&mut self, kind: PacketKind, body: &[u8]) -> Result { match kind { PacketKind::StreamOpen => { let open: StreamOpen = protocol::from_body(body)?; self.accept_open(open).map(TransportEvent::Open) } PacketKind::StreamOpenOk => { let ok: StreamOpenOk = protocol::from_body(body)?; let stream_id = ok.stream_id; let flushed = self.handle_open_ok(ok)?; Ok(TransportEvent::OpenOk { stream_id, flushed }) } PacketKind::StreamOpenReject => { let reject: StreamOpenReject = protocol::from_body(body)?; self.handle_open_reject(reject.clone()); Ok(TransportEvent::OpenReject { stream_id: reject.stream_id, reason: reject.reason, }) } PacketKind::StreamData => { let data: StreamData = protocol::from_body(body)?; self.handle_data(data).map(TransportEvent::Data) } PacketKind::StreamWindowAdjust => { let adjust: StreamWindowAdjust = protocol::from_body(body)?; let stream_id = adjust.stream_id; let flushed = self.handle_window_adjust(adjust)?; Ok(TransportEvent::WindowAdjust { stream_id, flushed }) } PacketKind::StreamClose => { let close: StreamClose = protocol::from_body(body)?; let stream_id = close.stream_id; self.handle_close(close); Ok(TransportEvent::Close { stream_id }) } _ => bail!("packet kind {kind:?} is not a stream transport packet"), } } pub fn tick(&mut self) -> Result> { let now = Instant::now(); let mut out = Vec::new(); let mut retransmit_opens = Vec::new(); for (stream_id, pending) in self.pending_opens.iter_mut() { if now.duration_since(pending.last_sent) < self.config.retransmit_after { continue; } pending.last_sent = now; pending.attempts = pending.attempts.saturating_add(1); retransmit_opens.push((*stream_id, pending.target_host.clone(), pending.target_port)); } for (stream_id, target_host, target_port) in retransmit_opens { out.push(encode_packet( PacketKind::StreamOpen, &StreamOpen { stream_id, target_host, target_port, }, )?); } let mut retransmit_data = Vec::new(); for (stream_id, chunks) in self.sent_data.iter_mut() { for chunk in chunks.values_mut() { if now.duration_since(chunk.last_sent) < self.config.retransmit_after { continue; } chunk.last_sent = now; chunk.attempts = chunk.attempts.saturating_add(1); retransmit_data.push((*stream_id, chunk.offset, chunk.bytes.clone())); } } for (stream_id, offset, bytes) in retransmit_data { out.push(encode_packet( PacketKind::StreamData, &StreamData { stream_id, offset, bytes, }, )?); } Ok(out) } pub fn is_open(&self, stream_id: u64) -> bool { self.opened_streams.contains(&stream_id) } pub fn pending_bytes(&self, stream_id: u64) -> usize { self.pending_data .get(&stream_id) .map(|queue| queue.iter().map(Vec::len).sum()) .unwrap_or(0) } fn send_data_now(&mut self, stream_id: u64, bytes: Vec) -> Result { let credit = self .send_credit .get_mut(&stream_id) .ok_or_else(|| anyhow!("stream {stream_id} has no send window"))?; if *credit < bytes.len() { bail!("stream {stream_id} send window exhausted"); } *credit -= bytes.len(); let offset = *self.next_send_offset.entry(stream_id).or_default(); self.next_send_offset .insert(stream_id, offset.saturating_add(bytes.len() as u64)); self.sent_data.entry(stream_id).or_default().insert( offset, PendingStreamChunk { offset, bytes: bytes.clone(), last_sent: Instant::now(), attempts: 1, }, ); encode_packet( PacketKind::StreamData, &StreamData { stream_id, offset, bytes, }, ) } fn flush_pending_data(&mut self, stream_id: u64) -> Result> { let mut out = Vec::new(); while let Some(front_len) = self .pending_data .get(&stream_id) .and_then(|pending| pending.front().map(Vec::len)) { if self.send_credit.get(&stream_id).copied().unwrap_or(0) < front_len { break; } let bytes = self .pending_data .get_mut(&stream_id) .and_then(VecDeque::pop_front) .expect("pending front exists"); if self .pending_data .get(&stream_id) .is_some_and(VecDeque::is_empty) { self.pending_data.remove(&stream_id); } out.push(self.send_data_now(stream_id, bytes)?); } Ok(out) } fn accept_data(&mut self, data: StreamData) -> (Vec>, usize, u64) { let stream_id = data.stream_id; let expected = self.next_recv_offset.entry(stream_id).or_insert(0); if data.offset < *expected { return (Vec::new(), 0, *expected); } if data.offset > *expected { self.recv_pending .entry(stream_id) .or_default() .entry(data.offset) .or_insert(data.bytes); return (Vec::new(), 0, *expected); } let mut chunks = vec![data.bytes]; let mut consumed = chunks[0].len(); *expected = expected.saturating_add(consumed as u64); while let Some(bytes) = self .recv_pending .entry(stream_id) .or_default() .remove(expected) { consumed = consumed.saturating_add(bytes.len()); *expected = expected.saturating_add(bytes.len() as u64); chunks.push(bytes); } if self .recv_pending .get(&stream_id) .is_some_and(BTreeMap::is_empty) { self.recv_pending.remove(&stream_id); } (chunks, consumed, *expected) } fn ack_data(&mut self, stream_id: u64, received_offset: u64) { let Some(sent) = self.sent_data.get_mut(&stream_id) else { return; }; let acked_offsets = sent .iter() .filter_map(|(offset, chunk)| { let end = chunk.offset.saturating_add(chunk.bytes.len() as u64); (end <= received_offset).then_some(*offset) }) .collect::>(); for offset in acked_offsets { sent.remove(&offset); } if sent.is_empty() { self.sent_data.remove(&stream_id); } } fn add_credit(&mut self, stream_id: u64, bytes: usize) { let credit = self.send_credit.entry(stream_id).or_default(); *credit = credit.saturating_add(bytes).min(self.config.initial_window); } fn cleanup_stream(&mut self, stream_id: u64) { self.opened_streams.remove(&stream_id); self.pending_opens.remove(&stream_id); self.send_credit.remove(&stream_id); self.pending_data.remove(&stream_id); self.sent_data.remove(&stream_id); self.next_send_offset.remove(&stream_id); self.next_recv_offset.remove(&stream_id); self.recv_pending.remove(&stream_id); } } pub struct DoshTransport { socket: Arc, role: SessionRole, conn_id: [u8; 16], session_key: [u8; 32], peer_addr: SocketAddr, send_seq: u64, ack_seq: u64, replay: ReplayWindow, mux: StreamMux, next_stream_id: u64, last_contact: Instant, last_keepalive: Instant, } impl DoshTransport { pub fn new(socket: Arc, config: SessionTransportConfig) -> Self { let now = Instant::now(); Self { socket, role: config.role, conn_id: config.conn_id, session_key: config.session_key, peer_addr: config.peer_addr, send_seq: config.initial_send_seq, ack_seq: config.initial_ack, replay: ReplayWindow::default(), mux: StreamMux::new(config.stream), next_stream_id: 1, last_contact: now, last_keepalive: now, } } pub fn new_owned(socket: UdpSocket, config: SessionTransportConfig) -> Self { Self::new(Arc::new(socket), config) } pub fn local_addr(&self) -> Result { Ok(self.socket.local_addr()?) } pub fn conn_id(&self) -> [u8; 16] { self.conn_id } pub fn mux(&self) -> &StreamMux { &self.mux } pub fn mux_mut(&mut self) -> &mut StreamMux { &mut self.mux } pub fn peer_addr(&self) -> SocketAddr { self.peer_addr } pub fn set_peer_addr(&mut self, peer_addr: SocketAddr) { self.peer_addr = peer_addr; } pub fn last_contact(&self) -> Instant { self.last_contact } pub fn stale_for(&self) -> Duration { self.last_contact.elapsed() } pub fn allocate_stream_id(&mut self) -> u64 { let id = self.next_stream_id; self.next_stream_id = self.next_stream_id.saturating_add(1).max(1); id } pub async fn open_service(&mut self, name: &str) -> Result { let stream_id = self.allocate_stream_id(); let target = service_target(name)?; let packet = self.mux.open_stream(stream_id, target, 0)?; self.send_outgoing(packet).await?; Ok(stream_id) } pub async fn open_target( &mut self, target_host: impl Into, target_port: u16, ) -> Result { let stream_id = self.allocate_stream_id(); let packet = self .mux .open_stream(stream_id, target_host.into(), target_port)?; self.send_outgoing(packet).await?; Ok(stream_id) } pub async fn accept_stream(&mut self, stream_id: u64) -> Result<()> { self.send_outgoing(StreamMux::open_ok(stream_id)?).await } pub async fn reject_stream(&mut self, stream_id: u64, reason: impl Into) -> Result<()> { self.send_outgoing(StreamMux::open_reject(stream_id, reason)?) .await } pub async fn send(&mut self, stream_id: u64, bytes: impl Into>) -> Result<()> { for packet in self.mux.send_data(stream_id, bytes)? { self.send_outgoing(packet).await?; } Ok(()) } pub async fn close(&mut self, stream_id: u64) -> Result<()> { let packet = self.mux.close_stream(stream_id)?; self.send_outgoing(packet).await } pub async fn maintenance(&mut self) -> Result<()> { for packet in self.mux.tick()? { self.send_outgoing(packet).await?; } if self.last_keepalive.elapsed() >= self.mux.config.keepalive_after { self.send_kind(PacketKind::Ping, b"").await?; self.last_keepalive = Instant::now(); } Ok(()) } pub async fn recv(&mut self) -> Result { let mut buf = vec![0u8; 65535]; loop { let (n, peer) = self.socket.recv_from(&mut buf).await?; let packet = match protocol::decode(&buf[..n]) { Ok(packet) => packet, Err(_) => continue, }; if packet.header.conn_id != self.conn_id { continue; } if !self.replay.accept(packet.header.seq) { continue; } let plain = protocol::decrypt_body(&packet, &self.session_key, self.role.recv_direction())?; self.peer_addr = peer; self.ack_seq = self.ack_seq.max(packet.header.seq); self.last_contact = Instant::now(); return self .handle_decrypted_packet(packet.header.kind, &plain) .await; } } pub async fn handle_datagram( &mut self, datagram: &[u8], peer: SocketAddr, ) -> Result { let packet = protocol::decode(datagram)?; if packet.header.conn_id != self.conn_id { return Ok(SessionEvent::Ignored); } if !self.replay.accept(packet.header.seq) { return Ok(SessionEvent::Ignored); } let plain = protocol::decrypt_body(&packet, &self.session_key, self.role.recv_direction())?; self.peer_addr = peer; self.ack_seq = self.ack_seq.max(packet.header.seq); self.last_contact = Instant::now(); self.handle_decrypted_packet(packet.header.kind, &plain) .await } async fn handle_decrypted_packet( &mut self, kind: PacketKind, plain: &[u8], ) -> Result { match kind { PacketKind::Ping => { self.send_kind(PacketKind::Pong, b"").await?; Ok(SessionEvent::Ping) } PacketKind::Pong => Ok(SessionEvent::Pong), PacketKind::StreamOpen | PacketKind::StreamOpenOk | PacketKind::StreamOpenReject | PacketKind::StreamData | PacketKind::StreamWindowAdjust | PacketKind::StreamClose => { let event = self.mux.handle_packet(kind, plain)?; self.send_followups(&event).await?; Ok(SessionEvent::Stream(event)) } _ => Ok(SessionEvent::Ignored), } } async fn send_followups(&mut self, event: &TransportEvent) -> Result<()> { match event { TransportEvent::OpenOk { flushed, .. } | TransportEvent::WindowAdjust { flushed, .. } => { for packet in flushed.clone() { self.send_outgoing(packet).await?; } } TransportEvent::Data(data) => { self.send_outgoing(data.window_adjust.clone()).await?; } TransportEvent::Open(_) | TransportEvent::OpenReject { .. } | TransportEvent::Close { .. } => {} } Ok(()) } async fn send_outgoing(&mut self, packet: OutgoingStreamPacket) -> Result<()> { self.send_kind(packet.kind, &packet.body).await } async fn send_kind(&mut self, kind: PacketKind, body: &[u8]) -> Result<()> { let encoded = self.encode_kind(kind, body)?; self.socket.send_to(&encoded, self.peer_addr).await?; Ok(()) } fn encode_kind(&mut self, kind: PacketKind, body: &[u8]) -> Result> { let seq = self.send_seq; self.send_seq = self.send_seq.saturating_add(1).max(1); protocol::encode_encrypted( kind, self.conn_id, seq, self.ack_seq, &self.session_key, self.role.send_direction(), body, ) } } pub fn service_target(name: &str) -> Result { if name.is_empty() || !name .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_')) { bail!("invalid Dosh service name {name:?}"); } Ok(format!("{SERVICE_TARGET_PREFIX}{name}")) } pub fn is_service_target(target: &str) -> bool { target.starts_with(SERVICE_TARGET_PREFIX) } pub fn service_name_from_target(target: &str) -> Option<&str> { target.strip_prefix(SERVICE_TARGET_PREFIX) } fn encode_packet(kind: PacketKind, value: &T) -> Result { Ok(OutgoingStreamPacket { kind, body: protocol::to_body(value)?, }) } #[cfg(test)] mod tests { use super::*; fn decode(packet: &OutgoingStreamPacket) -> T { protocol::from_body(&packet.body).unwrap() } #[test] fn service_targets_are_reserved_and_validated() { assert_eq!(service_target("chat_api").unwrap(), "@dosh-chat_api"); assert!(is_service_target("@dosh-file")); assert_eq!(service_name_from_target("@dosh-chat"), Some("chat")); assert!(service_target("../bad").is_err()); } #[test] fn service_registry_matches_registered_service_opens() { let mut registry = ServiceRegistry::new(); registry.register("chat").unwrap(); let accepted = registry .match_open(&IncomingStreamOpen { stream_id: 4, target_host: "@dosh-chat".to_string(), target_port: 0, }) .unwrap(); assert_eq!( accepted, AcceptedService { name: "chat".to_string(), stream_id: 4, } ); assert!( registry .match_open(&IncomingStreamOpen { stream_id: 5, target_host: "@dosh-other".to_string(), target_port: 0, }) .is_err() ); } #[test] fn opens_stream_and_flushes_queued_data_after_open_ok() { let mut mux = StreamMux::new(TransportConfig::default()); let open = mux .open_stream(7, service_target("chat").unwrap(), 0) .unwrap(); assert_eq!(open.kind, PacketKind::StreamOpen); assert!(mux.send_data(7, b"hello".to_vec()).unwrap().is_empty()); assert_eq!(mux.pending_bytes(7), 5); let flushed = mux.handle_open_ok(StreamOpenOk { stream_id: 7 }).unwrap(); assert_eq!(flushed.len(), 1); assert_eq!(flushed[0].kind, PacketKind::StreamData); let data: StreamData = decode(&flushed[0]); assert_eq!(data.offset, 0); assert_eq!(data.bytes, b"hello"); } #[test] fn receives_out_of_order_data_in_order_and_acks_offsets() { let mut mux = StreamMux::new(TransportConfig::default()); mux.accept_open(StreamOpen { stream_id: 3, target_host: "@dosh-test".to_string(), target_port: 0, }) .unwrap(); let first = mux .handle_data(StreamData { stream_id: 3, offset: 5, bytes: b"world".to_vec(), }) .unwrap(); assert!(first.chunks.is_empty()); assert_eq!(first.received_offset, 0); let second = mux .handle_data(StreamData { stream_id: 3, offset: 0, bytes: b"hello".to_vec(), }) .unwrap(); assert_eq!(second.chunks, vec![b"hello".to_vec(), b"world".to_vec()]); assert_eq!(second.consumed, 10); assert_eq!(second.received_offset, 10); } #[test] fn window_adjust_acks_sent_data_and_releases_credit() { let mut mux = StreamMux::new(TransportConfig { initial_window: 5, retransmit_after: Duration::from_secs(1), ..TransportConfig::default() }); mux.open_stream(1, "@dosh-test", 0).unwrap(); mux.handle_open_ok(StreamOpenOk { stream_id: 1 }).unwrap(); let sent = mux.send_data(1, b"hello".to_vec()).unwrap(); assert_eq!(sent.len(), 1); assert!(mux.send_data(1, b"!".to_vec()).unwrap().is_empty()); assert_eq!(mux.pending_bytes(1), 1); let flushed = mux .handle_window_adjust(StreamWindowAdjust { stream_id: 1, received_offset: 5, bytes: 5, }) .unwrap(); assert_eq!(flushed.len(), 1); let data: StreamData = decode(&flushed[0]); assert_eq!(data.offset, 5); assert_eq!(data.bytes, b"!"); } #[test] fn handle_packet_decodes_and_dispatches_stream_packets() { let mut mux = StreamMux::new(TransportConfig::default()); let body = protocol::to_body(&StreamOpen { stream_id: 9, target_host: "@dosh-app".to_string(), target_port: 0, }) .unwrap(); let event = mux.handle_packet(PacketKind::StreamOpen, &body).unwrap(); assert_eq!( event, TransportEvent::Open(IncomingStreamOpen { stream_id: 9, target_host: "@dosh-app".to_string(), target_port: 0, }) ); } #[tokio::test] async fn session_transport_opens_service_and_moves_data_over_encrypted_udp() { let client_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let client_addr = client_socket.local_addr().unwrap(); let server_addr = server_socket.local_addr().unwrap(); let key = [42u8; 32]; let conn_id = [7u8; 16]; let mut client = DoshTransport::new_owned( client_socket, SessionTransportConfig { role: SessionRole::Client, conn_id, session_key: key, peer_addr: server_addr, initial_send_seq: 1, initial_ack: 0, stream: TransportConfig::default(), }, ); let mut server = DoshTransport::new_owned( server_socket, SessionTransportConfig { role: SessionRole::Server, conn_id, session_key: key, peer_addr: client_addr, initial_send_seq: 1, initial_ack: 0, stream: TransportConfig::default(), }, ); let stream_id = client.open_service("app").await.unwrap(); let event = server.recv().await.unwrap(); assert_eq!(server.peer_addr(), client_addr); match event { SessionEvent::Stream(TransportEvent::Open(open)) => { assert_eq!(open.stream_id, stream_id); assert_eq!(open.target_host, "@dosh-app"); server.accept_stream(open.stream_id).await.unwrap(); } other => panic!("unexpected event {other:?}"), } assert!(matches!( client.recv().await.unwrap(), SessionEvent::Stream(TransportEvent::OpenOk { .. }) )); client.send(stream_id, b"hello".to_vec()).await.unwrap(); let event = server.recv().await.unwrap(); match event { SessionEvent::Stream(TransportEvent::Data(data)) => { assert_eq!(data.stream_id, stream_id); assert_eq!(data.chunks, vec![b"hello".to_vec()]); } other => panic!("unexpected event {other:?}"), } } #[tokio::test] async fn session_transport_roams_to_authenticated_new_peer_addr() { let server_socket = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let original_client = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let roaming_client = UdpSocket::bind("127.0.0.1:0").await.unwrap(); let original_addr = original_client.local_addr().unwrap(); let roaming_addr = roaming_client.local_addr().unwrap(); let server_addr = server_socket.local_addr().unwrap(); let key = [9u8; 32]; let conn_id = [3u8; 16]; let mut server = DoshTransport::new_owned( server_socket, SessionTransportConfig { role: SessionRole::Server, conn_id, session_key: key, peer_addr: original_addr, initial_send_seq: 1, initial_ack: 0, stream: TransportConfig::default(), }, ); let ping = protocol::encode_encrypted( PacketKind::Ping, conn_id, 1, 0, &key, CLIENT_TO_SERVER, b"", ) .unwrap(); roaming_client.send_to(&ping, server_addr).await.unwrap(); assert!(matches!(server.recv().await.unwrap(), SessionEvent::Ping)); assert_eq!(server.peer_addr(), roaming_addr); } }