1use std::time::Duration;
10
11use tokio::io::{AsyncRead, AsyncReadExt};
12use tokio::time::timeout;
13
14use crate::proto::fsp_wire::{FSP_FLAG_CP, FSP_FLAG_DIRECT_TRANSPORT, FSP_FLAG_U, FSP_HEADER_SIZE};
15
16const PHASE_ESTABLISHED: u8 = 0x0;
18const PHASE_MSG1: u8 = 0x1;
19const PHASE_MSG2: u8 = 0x2;
20
21const PREFIX_SIZE: usize = 4;
23
24const FMP_ESTABLISHED_REMAINING_HEADER: usize = 12;
29const DIRECT_FSP_REMAINING_HEADER: usize = FSP_HEADER_SIZE - PREFIX_SIZE;
30const AEAD_TAG_SIZE: usize = 16;
31
32pub(crate) const DEFAULT_FRAME_COMPLETION_TIMEOUT: Duration = Duration::from_secs(30);
35
36#[derive(Debug)]
38pub enum StreamError {
39 UnknownVersion(u8),
41 UnknownPhase(u8),
43 PayloadTooLarge {
45 payload_len: u16,
46 max_payload_len: u16,
47 },
48 HandshakeSizeMismatch { phase: u8, expected: u16, got: u16 },
50 Io(std::io::Error),
52}
53
54impl std::fmt::Display for StreamError {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 StreamError::UnknownVersion(v) => write!(f, "unknown FMP version: {}", v),
58 StreamError::UnknownPhase(p) => write!(f, "unknown FMP phase: 0x{:02x}", p),
59 StreamError::PayloadTooLarge {
60 payload_len,
61 max_payload_len,
62 } => {
63 write!(
64 f,
65 "payload_len {} exceeds max {}",
66 payload_len, max_payload_len
67 )
68 }
69 StreamError::HandshakeSizeMismatch {
70 phase,
71 expected,
72 got,
73 } => {
74 write!(
75 f,
76 "handshake phase 0x{:x}: expected payload_len {}, got {}",
77 phase, expected, got
78 )
79 }
80 StreamError::Io(e) => write!(f, "io: {}", e),
81 }
82 }
83}
84
85impl std::error::Error for StreamError {}
86
87impl From<std::io::Error> for StreamError {
88 fn from(e: std::io::Error) -> Self {
89 StreamError::Io(e)
90 }
91}
92
93const MSG1_WIRE_SIZE: usize = 114;
97const MSG2_WIRE_SIZE: usize = 69;
98
99const MSG1_PAYLOAD_LEN: u16 = (MSG1_WIRE_SIZE - PREFIX_SIZE) as u16;
101
102const MSG2_PAYLOAD_LEN: u16 = (MSG2_WIRE_SIZE - PREFIX_SIZE) as u16;
104
105fn invalid_data(message: impl Into<String>) -> StreamError {
106 StreamError::Io(std::io::Error::new(
107 std::io::ErrorKind::InvalidData,
108 message.into(),
109 ))
110}
111
112fn completion_timed_out() -> StreamError {
113 StreamError::Io(std::io::Error::new(
114 std::io::ErrorKind::TimedOut,
115 "stream frame completion timed out",
116 ))
117}
118
119fn stream_record_len(prefix: &[u8; PREFIX_SIZE]) -> Result<usize, StreamError> {
120 let version = prefix[0] >> 4;
121 let phase = prefix[0] & 0x0F;
122
123 if version != 0 {
124 return Err(StreamError::UnknownVersion(version));
125 }
126
127 let payload_len = u16::from_le_bytes([prefix[2], prefix[3]]);
128
129 let remaining = match phase {
130 PHASE_ESTABLISHED => {
131 let is_direct_fsp = prefix[1] & FSP_FLAG_DIRECT_TRANSPORT != 0;
132 if is_direct_fsp && prefix[1] & (FSP_FLAG_CP | FSP_FLAG_U) != 0 {
133 return Err(invalid_data(format!(
134 "invalid direct FSP flags: 0x{:02x}",
135 prefix[1]
136 )));
137 }
138 let remaining_header = if is_direct_fsp {
139 DIRECT_FSP_REMAINING_HEADER
140 } else {
141 FMP_ESTABLISHED_REMAINING_HEADER
142 };
143 remaining_header + payload_len as usize + AEAD_TAG_SIZE
144 }
145 PHASE_MSG1 => {
146 if payload_len != MSG1_PAYLOAD_LEN {
147 return Err(StreamError::HandshakeSizeMismatch {
148 phase,
149 expected: MSG1_PAYLOAD_LEN,
150 got: payload_len,
151 });
152 }
153 payload_len as usize
154 }
155 PHASE_MSG2 => {
156 if payload_len != MSG2_PAYLOAD_LEN {
157 return Err(StreamError::HandshakeSizeMismatch {
158 phase,
159 expected: MSG2_PAYLOAD_LEN,
160 got: payload_len,
161 });
162 }
163 payload_len as usize
164 }
165 _ => {
166 return Err(StreamError::UnknownPhase(phase));
167 }
168 };
169
170 Ok(PREFIX_SIZE + remaining)
171}
172
173pub(crate) fn validate_stream_record(data: &[u8]) -> Result<(), StreamError> {
175 let prefix: [u8; PREFIX_SIZE] = data
176 .get(..PREFIX_SIZE)
177 .ok_or_else(|| {
178 invalid_data(format!(
179 "record size mismatch: expected at least {PREFIX_SIZE}, got {}",
180 data.len()
181 ))
182 })?
183 .try_into()
184 .expect("four-byte prefix slice");
185 let expected = stream_record_len(&prefix)?;
186 if data.len() != expected {
187 return Err(invalid_data(format!(
188 "record size mismatch: expected {expected}, got {}",
189 data.len()
190 )));
191 }
192 Ok(())
193}
194
195pub async fn read_fmp_packet<R: AsyncRead + Unpin>(
202 reader: &mut R,
203 mtu: u16,
204) -> Result<Vec<u8>, StreamError> {
205 let mut prefix = [0u8; PREFIX_SIZE];
206 reader.read_exact(&mut prefix).await?;
207 let total = stream_record_len(&prefix)?;
208
209 if prefix[0] & 0x0f == PHASE_ESTABLISHED {
210 let payload_len = u16::from_le_bytes([prefix[2], prefix[3]]);
211 let remaining_header = if prefix[1] & FSP_FLAG_DIRECT_TRANSPORT != 0 {
212 DIRECT_FSP_REMAINING_HEADER
213 } else {
214 FMP_ESTABLISHED_REMAINING_HEADER
215 };
216 let max_payload_len =
217 mtu.saturating_sub((PREFIX_SIZE + remaining_header + AEAD_TAG_SIZE) as u16);
218 if payload_len > max_payload_len {
219 return Err(StreamError::PayloadTooLarge {
220 payload_len,
221 max_payload_len,
222 });
223 }
224 }
225
226 let mut packet = vec![0u8; total];
227 packet[..PREFIX_SIZE].copy_from_slice(&prefix);
228 reader.read_exact(&mut packet[PREFIX_SIZE..]).await?;
229 Ok(packet)
230}
231
232pub(crate) async fn read_fmp_packet_with_timeout<R: AsyncRead + Unpin>(
238 reader: &mut R,
239 completion_timeout: Duration,
240) -> Result<Vec<u8>, StreamError> {
241 let mut prefix = [0u8; PREFIX_SIZE];
242 reader.read_exact(&mut prefix[..1]).await?;
243
244 timeout(completion_timeout, async {
245 reader.read_exact(&mut prefix[1..]).await?;
246 let total = stream_record_len(&prefix)?;
247 let mut packet = vec![0u8; total];
248 packet[..PREFIX_SIZE].copy_from_slice(&prefix);
249 reader.read_exact(&mut packet[PREFIX_SIZE..]).await?;
250 Ok::<_, StreamError>(packet)
251 })
252 .await
253 .map_err(|_| completion_timed_out())?
254}
255
256#[cfg(test)]
261mod tests {
262 use super::*;
263 use std::io::Cursor;
264
265 const READ_TIMEOUT: Duration = Duration::from_secs(1);
266
267 async fn read_fmp_packet<R: AsyncRead + Unpin>(
268 reader: &mut R,
269 completion_timeout: Duration,
270 ) -> Result<Vec<u8>, StreamError> {
271 read_fmp_packet_with_timeout(reader, completion_timeout).await
272 }
273
274 fn build_established_frame(payload_len: u16) -> Vec<u8> {
277 let total =
278 PREFIX_SIZE + FMP_ESTABLISHED_REMAINING_HEADER + payload_len as usize + AEAD_TAG_SIZE;
279 let mut frame = vec![0u8; total];
280 frame[0] = 0x00; frame[1] = 0x00; frame[2..4].copy_from_slice(&payload_len.to_le_bytes());
283 for (i, byte) in frame[PREFIX_SIZE..total].iter_mut().enumerate() {
285 *byte = ((PREFIX_SIZE + i) & 0xFF) as u8;
286 }
287 frame
288 }
289
290 fn build_direct_fsp_frame(payload_len: u16) -> Vec<u8> {
293 let remaining_header = crate::proto::fsp_wire::FSP_HEADER_SIZE - PREFIX_SIZE;
294 let total = PREFIX_SIZE + remaining_header + payload_len as usize + AEAD_TAG_SIZE;
295 let mut frame = vec![0u8; total];
296 frame[0] = 0x00;
297 frame[1] = crate::proto::fsp_wire::FSP_FLAG_DIRECT_TRANSPORT;
298 frame[2..4].copy_from_slice(&payload_len.to_le_bytes());
299 for (i, byte) in frame[PREFIX_SIZE..total].iter_mut().enumerate() {
300 *byte = (0x80 | ((PREFIX_SIZE + i) & 0x7F)) as u8;
301 }
302 frame
303 }
304
305 fn build_msg1_frame() -> Vec<u8> {
307 let mut frame = vec![0xAA; MSG1_WIRE_SIZE];
308 frame[0] = 0x01; frame[1] = 0x00; frame[2..4].copy_from_slice(&MSG1_PAYLOAD_LEN.to_le_bytes());
311 frame
312 }
313
314 fn build_msg2_frame() -> Vec<u8> {
316 let mut frame = vec![0xBB; MSG2_WIRE_SIZE];
317 frame[0] = 0x02; frame[1] = 0x00; frame[2..4].copy_from_slice(&MSG2_PAYLOAD_LEN.to_le_bytes());
320 frame
321 }
322
323 #[tokio::test]
324 async fn test_read_established_frame() {
325 let payload_len = 64u16;
326 let frame = build_established_frame(payload_len);
327 let expected = frame.clone();
328
329 let mut cursor = Cursor::new(frame);
330 let packet = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
331 assert_eq!(packet, expected);
332 }
333
334 #[tokio::test]
335 async fn test_read_msg1_frame() {
336 let frame = build_msg1_frame();
337 let expected = frame.clone();
338
339 let mut cursor = Cursor::new(frame);
340 let packet = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
341 assert_eq!(packet.len(), MSG1_WIRE_SIZE);
342 assert_eq!(packet, expected);
343 }
344
345 #[tokio::test]
346 async fn test_read_msg2_frame() {
347 let frame = build_msg2_frame();
348 let expected = frame.clone();
349
350 let mut cursor = Cursor::new(frame);
351 let packet = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
352 assert_eq!(packet.len(), MSG2_WIRE_SIZE);
353 assert_eq!(packet, expected);
354 }
355
356 #[tokio::test]
357 async fn test_read_multiple_packets() {
358 let mut data = Vec::new();
359 let msg1 = build_msg1_frame();
360 let est = build_established_frame(32);
361 let msg2 = build_msg2_frame();
362 data.extend_from_slice(&msg1);
363 data.extend_from_slice(&est);
364 data.extend_from_slice(&msg2);
365
366 let mut cursor = Cursor::new(data);
367 let p1 = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
368 assert_eq!(p1.len(), MSG1_WIRE_SIZE);
369
370 let p2 = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
371 assert_eq!(p2, est);
372
373 let p3 = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
374 assert_eq!(p3.len(), MSG2_WIRE_SIZE);
375 }
376
377 #[tokio::test]
378 async fn direct_fsp_and_fmp_frames_keep_exact_stream_boundaries() {
379 let direct_fsp = build_direct_fsp_frame(23);
380 let fmp = build_established_frame(31);
381 let mut data = direct_fsp.clone();
382 data.extend_from_slice(&fmp);
383
384 let mut cursor = Cursor::new(data);
385 assert_eq!(
386 read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap(),
387 direct_fsp
388 );
389 assert_eq!(
390 read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap(),
391 fmp
392 );
393 }
394
395 #[tokio::test]
396 async fn direct_fsp_accepts_full_wire_range_and_rejects_bad_shape_or_flags() {
397 let max_payload = build_direct_fsp_frame(u16::MAX);
398 assert_eq!(
399 read_fmp_packet(&mut Cursor::new(max_payload.clone()), READ_TIMEOUT)
400 .await
401 .unwrap(),
402 max_payload
403 );
404
405 let mut truncated = build_direct_fsp_frame(23);
406 truncated.pop();
407 assert!(matches!(
408 read_fmp_packet(&mut Cursor::new(truncated), READ_TIMEOUT).await,
409 Err(StreamError::Io(_))
410 ));
411
412 let mut extra_byte = build_direct_fsp_frame(23);
413 extra_byte.push(0);
414 assert!(matches!(
415 validate_stream_record(&extra_byte),
416 Err(StreamError::Io(error)) if error.kind() == std::io::ErrorKind::InvalidData
417 ));
418
419 let mut invalid_flags = build_direct_fsp_frame(23);
420 invalid_flags[1] |= crate::proto::fsp_wire::FSP_FLAG_U;
421 assert!(matches!(
422 read_fmp_packet(&mut Cursor::new(invalid_flags), READ_TIMEOUT).await,
423 Err(StreamError::Io(error)) if error.kind() == std::io::ErrorKind::InvalidData
424 ));
425
426 let mut invalid_flags = build_direct_fsp_frame(23);
427 invalid_flags[1] |= crate::proto::fsp_wire::FSP_FLAG_CP;
428 assert!(matches!(
429 read_fmp_packet(&mut Cursor::new(invalid_flags), READ_TIMEOUT).await,
430 Err(StreamError::Io(error)) if error.kind() == std::io::ErrorKind::InvalidData
431 ));
432
433 let mut rekey_epoch = build_direct_fsp_frame(23);
434 rekey_epoch[1] |= crate::proto::fsp_wire::FSP_FLAG_K;
435 assert_eq!(
436 read_fmp_packet(&mut Cursor::new(rekey_epoch.clone()), READ_TIMEOUT)
437 .await
438 .unwrap(),
439 rekey_epoch,
440 "DIRECT and the encrypted epoch K bit are compatible"
441 );
442 }
443
444 #[tokio::test]
445 async fn test_unknown_version_error() {
446 let mut frame = vec![0u8; 100];
449 frame[0] = 0x16;
450 let mut cursor = Cursor::new(frame);
451 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
452 .await
453 .unwrap_err();
454 assert!(matches!(err, StreamError::UnknownVersion(1)));
455 }
456
457 #[tokio::test]
458 async fn test_unknown_phase_error() {
459 let mut frame = vec![0u8; 100];
460 frame[0] = 0x05; frame[2..4].copy_from_slice(&10u16.to_le_bytes());
462
463 let mut cursor = Cursor::new(frame);
464 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
465 .await
466 .unwrap_err();
467 assert!(matches!(err, StreamError::UnknownPhase(0x5)));
468 }
469
470 #[tokio::test]
471 async fn test_handshake_size_mismatch_msg1() {
472 let mut frame = vec![0u8; 200];
473 frame[0] = 0x01; frame[2..4].copy_from_slice(&50u16.to_le_bytes());
476
477 let mut cursor = Cursor::new(frame);
478 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
479 .await
480 .unwrap_err();
481 assert!(matches!(
482 err,
483 StreamError::HandshakeSizeMismatch { phase: 0x1, .. }
484 ));
485 }
486
487 #[tokio::test]
488 async fn test_handshake_size_mismatch_msg2() {
489 let mut frame = vec![0u8; 200];
490 frame[0] = 0x02; frame[2..4].copy_from_slice(&50u16.to_le_bytes());
493
494 let mut cursor = Cursor::new(frame);
495 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
496 .await
497 .unwrap_err();
498 assert!(matches!(
499 err,
500 StreamError::HandshakeSizeMismatch { phase: 0x2, .. }
501 ));
502 }
503
504 #[tokio::test]
505 async fn test_eof_on_prefix() {
506 let data = vec![0u8; 2];
508 let mut cursor = Cursor::new(data);
509 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
510 .await
511 .unwrap_err();
512 assert!(matches!(err, StreamError::Io(_)));
513 }
514
515 #[tokio::test]
516 async fn test_eof_on_body() {
517 let mut data = vec![0u8; 10]; data[0] = 0x01; data[2..4].copy_from_slice(&MSG1_PAYLOAD_LEN.to_le_bytes());
521
522 let mut cursor = Cursor::new(data);
523 let err = read_fmp_packet(&mut cursor, READ_TIMEOUT)
524 .await
525 .unwrap_err();
526 assert!(matches!(err, StreamError::Io(_)));
527 }
528
529 #[tokio::test]
530 async fn test_zero_payload_established() {
531 let frame = build_established_frame(0);
533 let expected_len = PREFIX_SIZE + FMP_ESTABLISHED_REMAINING_HEADER + AEAD_TAG_SIZE;
534 assert_eq!(frame.len(), expected_len);
535
536 let mut cursor = Cursor::new(frame.clone());
537 let packet = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
538 assert_eq!(packet.len(), expected_len);
539 assert_eq!(packet, frame);
540 }
541
542 #[tokio::test]
543 async fn test_max_fmp_payload_uses_the_full_wire_length() {
544 let frame = build_established_frame(u16::MAX);
545
546 let mut cursor = Cursor::new(frame.clone());
547 let packet = read_fmp_packet(&mut cursor, READ_TIMEOUT).await.unwrap();
548 assert_eq!(packet, frame);
549 validate_stream_record(&frame).unwrap();
550 }
551
552 #[tokio::test]
553 async fn public_reader_keeps_the_released_mtu_api_and_error_variant() {
554 let frame = build_established_frame(64);
555 assert_eq!(
556 super::read_fmp_packet(&mut Cursor::new(frame.clone()), 1400)
557 .await
558 .unwrap(),
559 frame
560 );
561
562 let over_budget = build_established_frame(1369);
563 assert!(matches!(
564 super::read_fmp_packet(&mut Cursor::new(over_budget), 1400).await,
565 Err(StreamError::PayloadTooLarge {
566 payload_len: 1369,
567 max_payload_len: 1368,
568 })
569 ));
570 }
571
572 #[tokio::test]
573 async fn idle_wait_is_unbounded_but_partial_prefix_and_body_are_bounded() {
574 let completion = Duration::from_millis(30);
575
576 let (mut writer, mut reader) = tokio::io::duplex(256);
577 let idle_read = tokio::spawn(async move { read_fmp_packet(&mut reader, completion).await });
578 tokio::time::sleep(completion + Duration::from_millis(10)).await;
579 let frame = build_direct_fsp_frame(7);
580 tokio::io::AsyncWriteExt::write_all(&mut writer, &frame)
581 .await
582 .unwrap();
583 assert_eq!(idle_read.await.unwrap().unwrap(), frame);
584
585 let (mut writer, mut reader) = tokio::io::duplex(256);
586 tokio::io::AsyncWriteExt::write_all(&mut writer, &[0x00])
587 .await
588 .unwrap();
589 assert!(matches!(
590 read_fmp_packet(&mut reader, completion).await,
591 Err(StreamError::Io(error)) if error.kind() == std::io::ErrorKind::TimedOut
592 ));
593
594 let (mut writer, mut reader) = tokio::io::duplex(256);
595 tokio::io::AsyncWriteExt::write_all(&mut writer, &[0x00, 0x00, 0x04, 0x00, 0xAA])
596 .await
597 .unwrap();
598 assert!(matches!(
599 read_fmp_packet(&mut reader, completion).await,
600 Err(StreamError::Io(error)) if error.kind() == std::io::ErrorKind::TimedOut
601 ));
602 }
603}