1use std::time::Duration;
15
16use gwk_domain::protocol::{
17 CONNECTION_BUDGET_WINDOW_SECS, FRAME_BODY_MAX_BYTES, FRAME_BODY_MIN_BYTES,
18 FRAME_KIND_RESERVED_STREAM, FRAME_LENGTH_PREFIX_BYTES, FrameKind, KernelErrorCode,
19};
20use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
21use tokio::time::Instant;
22
23use super::WireError;
24
25#[derive(Debug)]
50pub struct Budget {
51 ingress_per_window: usize,
52 egress_per_window: usize,
53 window: Duration,
54 ingress_spent: usize,
55 egress_spent: usize,
56 window_started: Instant,
57}
58
59impl Budget {
60 pub fn new(ingress: usize, egress: usize) -> Self {
61 Self::with_window(
62 ingress,
63 egress,
64 Duration::from_secs(CONNECTION_BUDGET_WINDOW_SECS),
65 )
66 }
67
68 pub fn with_window(ingress: usize, egress: usize, window: Duration) -> Self {
71 Self {
72 ingress_per_window: ingress,
73 egress_per_window: egress,
74 window,
75 ingress_spent: 0,
76 egress_spent: 0,
77 window_started: Instant::now(),
78 }
79 }
80
81 async fn spend_ingress(&mut self, bytes: usize) -> Result<(), WireError> {
82 self.spend(bytes, Direction::Ingress).await
83 }
84
85 async fn spend_egress(&mut self, bytes: usize) -> Result<(), WireError> {
86 self.spend(bytes, Direction::Egress).await
87 }
88
89 async fn spend(&mut self, bytes: usize, direction: Direction) -> Result<(), WireError> {
90 let per_window = match direction {
91 Direction::Ingress => self.ingress_per_window,
92 Direction::Egress => self.egress_per_window,
93 };
94 if bytes > per_window {
97 return Err(WireError::new(
98 KernelErrorCode::FrameSize,
99 format!(
100 "a {direction} frame of {bytes} bytes exceeds the whole \
101 {per_window}-byte window allowance"
102 ),
103 ));
104 }
105 loop {
106 let elapsed = self.window_started.elapsed();
107 if elapsed >= self.window {
108 self.ingress_spent = 0;
109 self.egress_spent = 0;
110 self.window_started = Instant::now();
111 }
112 let spent = match direction {
113 Direction::Ingress => &mut self.ingress_spent,
114 Direction::Egress => &mut self.egress_spent,
115 };
116 if *spent + bytes <= per_window {
117 *spent += bytes;
118 return Ok(());
119 }
120 tokio::time::sleep(self.window - elapsed).await;
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy)]
129enum Direction {
130 Ingress,
131 Egress,
132}
133
134impl std::fmt::Display for Direction {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.write_str(match self {
137 Self::Ingress => "received",
138 Self::Egress => "sent",
139 })
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
146pub struct Frame {
147 pub kind: FrameKind,
148 pub body: Vec<u8>,
149}
150
151#[derive(Debug)]
153pub enum Incoming {
154 Frame(Frame),
155 Closed,
156}
157
158pub async fn read_frame<R>(
165 reader: &mut R,
166 max_body: u32,
167 budget: &mut Budget,
168) -> Result<Incoming, WireError>
169where
170 R: AsyncRead + Unpin,
171{
172 let ceiling = max_body.min(FRAME_BODY_MAX_BYTES);
173 let mut prefix = [0u8; FRAME_LENGTH_PREFIX_BYTES];
174 match reader.read_exact(&mut prefix[..1]).await {
180 Ok(_) => {}
181 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(Incoming::Closed),
182 Err(e) => return Err(WireError::io("read frame length", e)),
183 }
184 reader
185 .read_exact(&mut prefix[1..])
186 .await
187 .map_err(|e| WireError::io("read frame length", e))?;
188 let announced = u32::from_be_bytes(prefix);
189
190 if announced < FRAME_BODY_MIN_BYTES {
191 return Err(WireError::new(
195 KernelErrorCode::FrameSize,
196 format!(
197 "frame body_length {announced} is below the {FRAME_BODY_MIN_BYTES}-byte minimum"
198 ),
199 ));
200 }
201 if announced > ceiling {
202 return Err(WireError::new(
203 KernelErrorCode::FrameSize,
204 format!("frame body_length {announced} exceeds the {ceiling}-byte maximum"),
205 ));
206 }
207
208 let total = FRAME_LENGTH_PREFIX_BYTES + announced as usize;
209 budget.spend_ingress(total).await?;
210
211 let mut body = vec![0u8; announced as usize];
212 reader
213 .read_exact(&mut body)
214 .await
215 .map_err(|e| WireError::io("read frame body", e))?;
216
217 let kind_byte = body.remove(0);
221 let kind = FrameKind::from_u8(kind_byte).ok_or_else(|| {
222 let detail = if kind_byte == FRAME_KIND_RESERVED_STREAM {
223 " (reserved for the terminal engine and not accepted in v1)"
224 } else {
225 ""
226 };
227 WireError::new(
228 KernelErrorCode::Handshake,
229 format!("unknown frame kind 0x{kind_byte:02x}{detail}"),
230 )
231 })?;
232 Ok(Incoming::Frame(Frame { kind, body }))
233}
234
235pub async fn write_frame<W>(
237 writer: &mut W,
238 kind: FrameKind,
239 body: &[u8],
240 budget: &mut Budget,
241) -> Result<(), WireError>
242where
243 W: AsyncWrite + Unpin,
244{
245 let announced = u32::try_from(body.len() + 1).map_err(|_| {
248 WireError::new(
249 KernelErrorCode::FrameSize,
250 format!("frame body {} bytes does not fit a u32 length", body.len()),
251 )
252 })?;
253 if announced > FRAME_BODY_MAX_BYTES {
254 return Err(WireError::new(
255 KernelErrorCode::FrameSize,
256 format!(
257 "frame body_length {announced} exceeds the {FRAME_BODY_MAX_BYTES}-byte maximum"
258 ),
259 ));
260 }
261 budget
262 .spend_egress(FRAME_LENGTH_PREFIX_BYTES + announced as usize)
263 .await?;
264
265 let mut out = Vec::with_capacity(FRAME_LENGTH_PREFIX_BYTES + announced as usize);
269 out.extend_from_slice(&announced.to_be_bytes());
270 out.push(kind.as_u8());
271 out.extend_from_slice(body);
272 writer
273 .write_all(&out)
274 .await
275 .map_err(|e| WireError::io("write frame", e))?;
276 writer
277 .flush()
278 .await
279 .map_err(|e| WireError::io("flush frame", e))
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 fn budget() -> Budget {
287 Budget::new(1 << 20, 1 << 20)
288 }
289
290 async fn read_bytes(raw: &[u8], max_body: u32) -> Result<Incoming, WireError> {
291 read_frame(
292 &mut std::io::Cursor::new(raw.to_vec()),
293 max_body,
294 &mut budget(),
295 )
296 .await
297 }
298
299 #[tokio::test]
300 async fn a_frame_survives_its_own_round_trip() {
301 let mut wire = Vec::new();
302 let mut out = budget();
303 write_frame(
304 &mut wire,
305 FrameKind::Json,
306 b"{\"type\":\"health\"}",
307 &mut out,
308 )
309 .await
310 .expect("write");
311 assert_eq!(&wire[..4], &18u32.to_be_bytes());
313 assert_eq!(wire[4], FrameKind::Json.as_u8());
314
315 match read_bytes(&wire, FRAME_BODY_MAX_BYTES).await.expect("read") {
316 Incoming::Frame(frame) => {
317 assert_eq!(frame.kind, FrameKind::Json);
318 assert_eq!(frame.body, b"{\"type\":\"health\"}");
319 }
320 Incoming::Closed => panic!("closed on a whole frame"),
321 }
322 }
323
324 fn block_on<F: Future>(future: F) -> F::Output {
327 tokio::runtime::Builder::new_current_thread()
328 .build()
329 .expect("a current-thread runtime")
330 .block_on(future)
331 }
332
333 proptest::proptest! {
334 #[test]
343 fn any_body_survives_the_round_trip(body in proptest::collection::vec(proptest::num::u8::ANY, 0..8192)) {
344 let read = block_on(async {
345 let mut wire = Vec::new();
346 let mut out = budget();
347 write_frame(&mut wire, FrameKind::Json, &body, &mut out).await.expect("write");
348 assert_eq!(&wire[..4], &(body.len() as u32 + 1).to_be_bytes());
352 read_bytes(&wire, FRAME_BODY_MAX_BYTES).await.expect("read")
353 });
354 match read {
355 Incoming::Frame(frame) => {
356 proptest::prop_assert_eq!(frame.kind, FrameKind::Json);
357 proptest::prop_assert_eq!(frame.body, body);
358 }
359 Incoming::Closed => proptest::prop_assert!(false, "closed on a whole frame"),
360 }
361 }
362
363 #[test]
368 fn no_other_kind_byte_is_admitted(kind in proptest::num::u8::ANY) {
369 proptest::prop_assume!(kind != FrameKind::Json.as_u8());
370 let mut wire = 2u32.to_be_bytes().to_vec();
371 wire.push(kind);
372 wire.push(b'x');
373 let error = block_on(read_bytes(&wire, FRAME_BODY_MAX_BYTES))
374 .expect_err("an unknown kind byte was admitted");
375 proptest::prop_assert_eq!(error.code, KernelErrorCode::Handshake);
376 }
377
378 #[test]
383 fn arbitrary_bytes_are_answered_and_never_panic(raw in proptest::collection::vec(proptest::num::u8::ANY, 0..512)) {
384 let _ = block_on(read_bytes(&raw, FRAME_BODY_MAX_BYTES));
387 }
388 }
389
390 #[tokio::test]
391 async fn an_illegal_length_is_refused_from_the_prefix_alone() {
392 for (announced, expected) in [(0u32, "below"), (FRAME_BODY_MAX_BYTES + 1, "exceeds")] {
396 let error = read_bytes(&announced.to_be_bytes(), FRAME_BODY_MAX_BYTES)
397 .await
398 .expect_err("illegal length accepted");
399 assert_eq!(error.code, KernelErrorCode::FrameSize);
400 assert!(error.message.contains(expected), "{error}");
401 }
402 }
403
404 #[tokio::test]
405 async fn the_hello_cap_is_the_same_codec_with_a_lower_ceiling() {
406 let announced = gwk_domain::protocol::HELLO_MAX_BYTES + 1;
407 let error = read_bytes(
408 &announced.to_be_bytes(),
409 gwk_domain::protocol::HELLO_MAX_BYTES,
410 )
411 .await
412 .expect_err("oversized hello accepted");
413 assert_eq!(error.code, KernelErrorCode::FrameSize);
414 let mut whole = announced.to_be_bytes().to_vec();
416 whole.push(FrameKind::Json.as_u8());
417 whole.extend(std::iter::repeat_n(b' ', announced as usize - 1));
418 assert!(matches!(
419 read_bytes(&whole, FRAME_BODY_MAX_BYTES)
420 .await
421 .expect("read"),
422 Incoming::Frame(_)
423 ));
424 }
425
426 #[tokio::test]
427 async fn the_reserved_engine_kind_is_refused_by_name() {
428 let mut raw = 1u32.to_be_bytes().to_vec();
429 raw.push(FRAME_KIND_RESERVED_STREAM);
430 let error = read_bytes(&raw, FRAME_BODY_MAX_BYTES)
431 .await
432 .expect_err("reserved kind accepted");
433 assert_eq!(error.code, KernelErrorCode::Handshake);
434 assert!(error.message.contains("terminal engine"), "{error}");
435 }
436
437 #[tokio::test]
438 async fn a_clean_hangup_between_frames_is_not_an_error() {
439 assert!(matches!(
440 read_bytes(&[], FRAME_BODY_MAX_BYTES).await.expect("eof"),
441 Incoming::Closed
442 ));
443 assert!(read_bytes(&[0, 0], FRAME_BODY_MAX_BYTES).await.is_err());
445 }
446
447 #[tokio::test(start_paused = true)]
448 async fn a_peer_that_outruns_its_allowance_waits_instead_of_dying() {
449 let mut small = Budget::new(12, 12);
452 let mut wire = Vec::new();
453 let mut writing = Budget::new(1 << 20, 1 << 20);
454 write_frame(&mut wire, FrameKind::Json, b"ab", &mut writing)
455 .await
456 .expect("write");
457 let mut stream = std::io::Cursor::new([wire.clone(), wire].concat());
458
459 let started = Instant::now();
460 read_frame(&mut stream, FRAME_BODY_MAX_BYTES, &mut small)
461 .await
462 .expect("first frame");
463 assert!(
466 started.elapsed() < Duration::from_millis(1),
467 "the first frame should not have waited"
468 );
469
470 read_frame(&mut stream, FRAME_BODY_MAX_BYTES, &mut small)
471 .await
472 .expect("the second frame arrives after the refill");
473 assert!(
477 started.elapsed() >= Duration::from_secs(CONNECTION_BUDGET_WINDOW_SECS),
478 "the second frame was not made to wait for its window"
479 );
480 }
481
482 #[tokio::test]
483 async fn a_frame_larger_than_a_whole_window_fails_rather_than_hanging() {
484 let mut sink = Vec::new();
485 let mut small = Budget::new(0, 6);
489 let error = write_frame(&mut sink, FrameKind::Json, b"abcd", &mut small)
490 .await
491 .expect_err("an unaffordable frame was not refused");
492 assert_eq!(error.code, KernelErrorCode::FrameSize);
493 assert!(error.message.contains("sent"), "{error}");
494 assert!(sink.is_empty());
497 }
498}