1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
2use std::time::{Duration, Instant};
3
4use crate::std::sync::Arc;
5
6use super::IdleGuard;
7
8use rama_core::graceful::ShutdownGuard;
9use rama_core::rt::Executor;
10use rama_core::telemetry::tracing;
11use rama_core::{
12 Service,
13 io::{BridgeIo, Io},
14};
15use rama_utils::macros::generate_set_and_with;
16use rama_utils::octets::kib;
17
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::sync::Notify;
20
21#[doc(inline)]
24pub use rama_core::stream::BridgeCloseReason;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum CopyDirection {
30 LeftToRight,
31 RightToLeft,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
37#[non_exhaustive]
38pub enum FirstByteTimeoutStart {
39 BridgeOpen,
46 #[default]
55 ClientFirstByte,
56}
57
58const DEFAULT_BUF_SIZE: usize = kib(16);
65const DEFAULT_SHUTDOWN_GRACE: Duration = Duration::from_millis(50);
66
67#[derive(Debug, Clone)]
77pub struct IoForwardService {
78 executor: Executor,
79 idle_timeout: Option<Duration>,
80 first_byte_timeout: Option<Duration>,
81 first_byte_timeout_start: FirstByteTimeoutStart,
82 shutdown_grace: Duration,
83 buf_size: usize,
84}
85
86impl Default for IoForwardService {
87 fn default() -> Self {
88 Self::new(Executor::default())
89 }
90}
91
92impl IoForwardService {
93 #[must_use]
95 pub fn new(executor: Executor) -> Self {
96 Self {
97 executor,
98 idle_timeout: None,
99 first_byte_timeout: None,
100 first_byte_timeout_start: FirstByteTimeoutStart::default(),
101 shutdown_grace: DEFAULT_SHUTDOWN_GRACE,
102 buf_size: DEFAULT_BUF_SIZE,
103 }
104 }
105
106 generate_set_and_with! {
107 pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
113 self.idle_timeout = timeout;
114 self
115 }
116 }
117
118 generate_set_and_with! {
119 pub fn first_byte_timeout(mut self, timeout: Option<Duration>) -> Self {
136 self.first_byte_timeout = timeout;
137 self
138 }
139 }
140
141 generate_set_and_with! {
142 pub fn first_byte_timeout_start(mut self, start: FirstByteTimeoutStart) -> Self {
148 self.first_byte_timeout_start = start;
149 self
150 }
151 }
152
153 generate_set_and_with! {
154 pub fn shutdown_grace(mut self, grace: Duration) -> Self {
161 self.shutdown_grace = grace;
162 self
163 }
164 }
165
166 generate_set_and_with! {
167 pub fn buf_size(mut self, size: usize) -> Self {
171 self.buf_size = size.max(1);
172 self
173 }
174 }
175
176 fn shutdown_guard(&self) -> Option<ShutdownGuard> {
178 self.executor.guard().cloned()
179 }
180}
181
182impl<S, T> Service<BridgeIo<S, T>> for IoForwardService
183where
184 S: Io + Unpin,
185 T: Io + Unpin,
186{
187 type Output = IoForwardOutcome;
188 type Error = IoForwardError;
189
190 async fn serve(
191 &self,
192 BridgeIo(left, right): BridgeIo<S, T>,
193 ) -> Result<Self::Output, Self::Error> {
194 #[cfg(feature = "dial9")]
195 super::dial9::record_bridge_opened(
196 self.idle_timeout
197 .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
198 .unwrap_or(0),
199 self.executor.guard().is_some(),
200 );
201
202 let outcome = run_bridge(
203 left,
204 right,
205 self.shutdown_guard(),
206 self.idle_timeout,
207 self.first_byte_timeout,
208 self.first_byte_timeout_start,
209 self.shutdown_grace,
210 self.buf_size,
211 )
212 .await;
213
214 emit_close_event(&outcome);
215
216 #[cfg(feature = "dial9")]
217 {
218 let age_ms = u64::try_from(outcome.age.as_millis()).unwrap_or(u64::MAX);
219 super::dial9::record_bridge_closed(
220 outcome.reason,
221 age_ms,
222 outcome.bytes_l_to_r,
223 outcome.bytes_r_to_l,
224 outcome.fatal_error.as_ref(),
225 );
226 }
227
228 let errored = outcome
232 .fatal_error
233 .as_ref()
234 .is_some_and(|err| !crate::conn::is_connection_error(err));
235 if errored {
236 Err(IoForwardError(outcome))
237 } else {
238 Ok(outcome)
239 }
240 }
241}
242
243#[derive(Debug)]
253pub struct IoForwardOutcome {
254 reason: BridgeCloseReason,
255 bytes_l_to_r: u64,
256 bytes_r_to_l: u64,
257 age: Duration,
258 fatal_error: Option<std::io::Error>,
259}
260
261impl IoForwardOutcome {
262 #[must_use]
264 pub fn reason(&self) -> BridgeCloseReason {
265 self.reason
266 }
267
268 #[must_use]
270 pub fn bytes_l_to_r(&self) -> u64 {
271 self.bytes_l_to_r
272 }
273
274 #[must_use]
276 pub fn bytes_r_to_l(&self) -> u64 {
277 self.bytes_r_to_l
278 }
279
280 #[must_use]
282 pub fn bytes_total(&self) -> u64 {
283 self.bytes_l_to_r.saturating_add(self.bytes_r_to_l)
284 }
285
286 #[must_use]
288 pub fn age(&self) -> Duration {
289 self.age
290 }
291
292 #[must_use]
294 pub fn fatal_error(&self) -> Option<&std::io::Error> {
295 self.fatal_error.as_ref()
296 }
297}
298
299impl std::fmt::Display for IoForwardOutcome {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 write!(
302 f,
303 "(proxy) I/O forwarder closed: reason={}, bytes_l_to_r={}, bytes_r_to_l={}, age_ms={}",
304 self.reason,
305 self.bytes_l_to_r,
306 self.bytes_r_to_l,
307 u64::try_from(self.age.as_millis()).unwrap_or(u64::MAX),
308 )?;
309 if let Some(err) = &self.fatal_error {
310 write!(f, ", error={err}")?;
311 }
312 Ok(())
313 }
314}
315
316#[derive(Debug)]
325pub struct IoForwardError(IoForwardOutcome);
326
327impl IoForwardError {
328 #[must_use]
330 pub fn outcome(&self) -> &IoForwardOutcome {
331 &self.0
332 }
333
334 #[must_use]
336 pub fn into_outcome(self) -> IoForwardOutcome {
337 self.0
338 }
339}
340
341impl std::ops::Deref for IoForwardError {
342 type Target = IoForwardOutcome;
343
344 fn deref(&self) -> &Self::Target {
345 &self.0
346 }
347}
348
349impl std::fmt::Display for IoForwardError {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 std::fmt::Display::fmt(&self.0, f)
352 }
353}
354
355impl std::error::Error for IoForwardError {
356 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
357 self.0
358 .fatal_error()
359 .map(|err| err as &(dyn std::error::Error + 'static))
360 }
361}
362
363#[expect(clippy::too_many_arguments)]
364async fn run_bridge<S, T>(
365 left: S,
366 right: T,
367 guard: Option<ShutdownGuard>,
368 idle_timeout: Option<Duration>,
369 first_byte_timeout: Option<Duration>,
370 first_byte_timeout_start: FirstByteTimeoutStart,
371 shutdown_grace: Duration,
372 buf_size: usize,
373) -> IoForwardOutcome
374where
375 S: Io + Unpin,
376 T: Io + Unpin,
377{
378 let opened_at = Instant::now();
379 let bytes_l_to_r = Arc::new(AtomicU64::new(0));
380 let bytes_r_to_l = Arc::new(AtomicU64::new(0));
381 let progress = Arc::new(AtomicU64::new(0));
382
383 let first_byte_seen = Arc::new(AtomicBool::new(false));
384 let upstream_eof_seen = Arc::new(AtomicBool::new(false));
385
386 let client_first_byte_seen = Arc::new(AtomicBool::new(false));
387 let client_spoke = Arc::new(Notify::new());
388
389 let (mut left_r, mut left_w) = tokio::io::split(left);
390 let (mut right_r, mut right_w) = tokio::io::split(right);
391
392 let left_w_shut = Arc::new(AtomicBool::new(false));
399 let right_w_shut = Arc::new(AtomicBool::new(false));
400
401 let (reason, fatal_error) = {
402 let l_to_r = std::pin::pin!(copy_one_way(
403 &mut left_r,
404 &mut right_w,
405 bytes_l_to_r.clone(),
406 progress.clone(),
407 buf_size,
408 shutdown_grace,
409 right_w_shut.clone(),
410 Some(client_first_byte_seen.clone()),
411 Some(client_spoke.clone()),
412 None,
413 ));
414 let r_to_l = std::pin::pin!(copy_one_way(
415 &mut right_r,
416 &mut left_w,
417 bytes_r_to_l.clone(),
418 progress.clone(),
419 buf_size,
420 shutdown_grace,
421 left_w_shut.clone(),
422 Some(first_byte_seen.clone()),
423 None,
424 Some(upstream_eof_seen.clone()),
425 ));
426
427 run_select_loop(
428 l_to_r,
429 r_to_l,
430 guard.as_ref(),
431 idle_timeout,
432 first_byte_timeout,
433 first_byte_timeout_start,
434 &progress,
435 &first_byte_seen,
436 &upstream_eof_seen,
437 &client_spoke,
438 )
439 .await
440 };
442
443 let left_pending_shutdown = !left_w_shut.load(Ordering::Acquire);
449 let right_pending_shutdown = !right_w_shut.load(Ordering::Acquire);
450 match (left_pending_shutdown, right_pending_shutdown) {
451 (true, true) => {
452 _ = tokio::join!(
453 tokio::time::timeout(shutdown_grace, left_w.shutdown()),
454 tokio::time::timeout(shutdown_grace, right_w.shutdown()),
455 );
456 }
457 (true, false) => {
458 _ = tokio::time::timeout(shutdown_grace, left_w.shutdown()).await;
459 }
460 (false, true) => {
461 _ = tokio::time::timeout(shutdown_grace, right_w.shutdown()).await;
462 }
463 (false, false) => {}
464 }
465
466 IoForwardOutcome {
467 reason,
468 bytes_l_to_r: bytes_l_to_r.load(Ordering::Relaxed),
469 bytes_r_to_l: bytes_r_to_l.load(Ordering::Relaxed),
470 age: opened_at.elapsed(),
471 fatal_error,
472 }
473}
474
475enum FirstByteWindow {
476 Inert,
478 PendingClient(Duration),
480 Armed(std::pin::Pin<Box<tokio::time::Sleep>>),
482}
483
484#[expect(clippy::too_many_arguments)]
485async fn run_select_loop<F1, F2>(
486 mut l_to_r: std::pin::Pin<&mut F1>,
487 mut r_to_l: std::pin::Pin<&mut F2>,
488 guard: Option<&ShutdownGuard>,
489 idle_timeout: Option<Duration>,
490 first_byte_timeout: Option<Duration>,
491 first_byte_timeout_start: FirstByteTimeoutStart,
492 progress: &AtomicU64,
493 first_byte_seen: &AtomicBool,
494 upstream_eof_seen: &AtomicBool,
495 client_spoke: &Notify,
496) -> (BridgeCloseReason, Option<std::io::Error>)
497where
498 F1: Future<Output = Result<(), std::io::Error>>,
499 F2: Future<Output = Result<(), std::io::Error>>,
500{
501 let mut idle = idle_timeout.map(IdleGuard::new);
502 let mut first_byte = match (first_byte_timeout, first_byte_timeout_start) {
503 (None, _) => FirstByteWindow::Inert,
504 (Some(d), FirstByteTimeoutStart::BridgeOpen) => {
505 FirstByteWindow::Armed(Box::pin(tokio::time::sleep(d)))
506 }
507 (Some(d), FirstByteTimeoutStart::ClientFirstByte) => FirstByteWindow::PendingClient(d),
508 };
509 let mut last_progress: u64 = 0;
510 let mut l_to_r_done = false;
511 let mut r_to_l_done = false;
512 let mut first_eof: Option<BridgeCloseReason> = None;
519
520 loop {
521 if l_to_r_done && r_to_l_done {
522 return (first_eof.unwrap_or(BridgeCloseReason::PeerEofLeft), None);
523 }
524
525 if !matches!(first_byte, FirstByteWindow::Inert)
531 && (first_byte_seen.load(Ordering::Relaxed)
532 || upstream_eof_seen.load(Ordering::Relaxed))
533 {
534 first_byte = FirstByteWindow::Inert;
535 }
536
537 let pending_client_window = match &first_byte {
538 FirstByteWindow::PendingClient(d) => Some(*d),
539 _ => None,
540 };
541
542 let cancelled = async {
543 match guard {
544 Some(g) => g.cancelled().await,
545 None => std::future::pending().await,
546 }
547 };
548
549 tokio::select! {
550 biased;
551 () = cancelled => return (BridgeCloseReason::Shutdown, None),
552 _ = async {
553 match idle.as_mut() {
554 Some(g) => g.tick().await,
555 None => std::future::pending().await,
556 }
557 } => {
558 let cur = progress.load(Ordering::Relaxed);
559 if cur != last_progress {
560 last_progress = cur;
561 if let Some(g) = idle.as_mut() {
562 g.reset();
563 }
564 continue;
565 }
566 return (BridgeCloseReason::IdleTimeout, None);
567 }
568 _ = async {
569 match &mut first_byte {
570 FirstByteWindow::Armed(s) => s.as_mut().await,
571 _ => std::future::pending().await,
572 }
573 } => {
574 if first_byte_seen.load(Ordering::Relaxed)
576 || upstream_eof_seen.load(Ordering::Relaxed)
577 {
578 first_byte = FirstByteWindow::Inert;
579 continue;
580 }
581 return (BridgeCloseReason::FirstByteTimeout, None);
582 }
583 _ = async {
584 match pending_client_window {
585 Some(_) => client_spoke.notified().await,
586 None => std::future::pending().await,
587 }
588 } => {
589 if let Some(d) = pending_client_window {
591 first_byte = FirstByteWindow::Armed(Box::pin(tokio::time::sleep(d)));
592 }
593 }
594 res = l_to_r.as_mut(), if !l_to_r_done => match res {
595 Ok(()) => {
596 l_to_r_done = true;
597 if first_eof.is_none() {
598 first_eof = Some(BridgeCloseReason::PeerEofLeft);
599 }
600 if !r_to_l_done {
601 continue;
602 }
603 return (
604 first_eof.unwrap_or(BridgeCloseReason::PeerEofLeft),
605 None,
606 );
607 }
608 Err(e) => {
609 let reason = classify_copy_error(&e, CopyDirection::LeftToRight);
610 return (reason, Some(e));
611 }
612 },
613 res = r_to_l.as_mut(), if !r_to_l_done => match res {
614 Ok(()) => {
615 r_to_l_done = true;
616 if first_eof.is_none() {
617 first_eof = Some(BridgeCloseReason::PeerEofRight);
618 }
619 if !l_to_r_done {
620 continue;
621 }
622 return (
623 first_eof.unwrap_or(BridgeCloseReason::PeerEofRight),
624 None,
625 );
626 }
627 Err(e) => {
628 let reason = classify_copy_error(&e, CopyDirection::RightToLeft);
629 return (reason, Some(e));
630 }
631 },
632 }
633 }
634}
635
636#[expect(clippy::too_many_arguments)]
637async fn copy_one_way<R, W>(
638 reader: &mut R,
639 writer: &mut W,
640 bytes: Arc<AtomicU64>,
641 progress: Arc<AtomicU64>,
642 buf_size: usize,
643 shutdown_grace: Duration,
644 write_side_shut: Arc<AtomicBool>,
645 first_byte_seen: Option<Arc<AtomicBool>>,
646 first_byte_notify: Option<Arc<Notify>>,
647 eof_seen: Option<Arc<AtomicBool>>,
648) -> Result<(), std::io::Error>
649where
650 R: tokio::io::AsyncRead + Unpin,
651 W: tokio::io::AsyncWrite + Unpin,
652{
653 let mut buf = vec![0u8; buf_size];
654 let mut copy_err: Option<std::io::Error> = None;
655 loop {
656 match reader.read(&mut buf).await {
657 Ok(0) => {
658 if let Some(seen) = &eof_seen {
659 seen.store(true, Ordering::Relaxed);
660 }
661 break;
662 }
663 Ok(n) => {
664 if let Some(seen) = &first_byte_seen
668 && !seen.swap(true, Ordering::Relaxed)
669 && let Some(notify) = &first_byte_notify
670 {
671 notify.notify_one();
672 }
673 if let Err(err) = writer.write_all(&buf[..n]).await {
674 copy_err = Some(err);
675 break;
676 }
677 bytes.fetch_add(n as u64, Ordering::Relaxed);
678 progress.fetch_add(1, Ordering::Relaxed);
679 }
680 Err(err) => {
681 copy_err = Some(err);
682 break;
683 }
684 }
685 }
686
687 _ = tokio::time::timeout(shutdown_grace, writer.shutdown()).await;
695 write_side_shut.store(true, Ordering::Release);
696
697 match copy_err {
698 Some(err) => Err(err),
699 None => Ok(()),
700 }
701}
702
703fn classify_copy_error(err: &std::io::Error, direction: CopyDirection) -> BridgeCloseReason {
704 use std::io::ErrorKind;
705
706 let read_side = matches!(
710 err.kind(),
711 ErrorKind::UnexpectedEof
712 | ErrorKind::ConnectionReset
713 | ErrorKind::ConnectionAborted
714 | ErrorKind::NotConnected
715 | ErrorKind::BrokenPipe
716 );
717 match (direction, read_side) {
718 (CopyDirection::LeftToRight, true) => BridgeCloseReason::ReadErrorLeft,
719 (CopyDirection::LeftToRight, false) => BridgeCloseReason::WriteErrorRight,
720 (CopyDirection::RightToLeft, true) => BridgeCloseReason::ReadErrorRight,
721 (CopyDirection::RightToLeft, false) => BridgeCloseReason::WriteErrorLeft,
722 }
723}
724
725fn emit_close_event(outcome: &IoForwardOutcome) {
726 let age_ms = u64::try_from(outcome.age.as_millis()).unwrap_or(u64::MAX);
727 if outcome.fatal_error.is_some() {
728 tracing::debug!(
729 target: "rama_net::proxy::forward",
730 reason = %outcome.reason,
731 bytes_l_to_r = outcome.bytes_l_to_r,
732 bytes_r_to_l = outcome.bytes_r_to_l,
733 age_ms,
734 error = ?outcome.fatal_error,
735 "io forward bridge closed",
736 );
737 } else {
738 tracing::trace!(
739 target: "rama_net::proxy::forward",
740 reason = %outcome.reason,
741 bytes_l_to_r = outcome.bytes_l_to_r,
742 bytes_r_to_l = outcome.bytes_r_to_l,
743 age_ms,
744 "io forward bridge closed",
745 );
746 }
747}
748
749#[cfg(test)]
750mod tests {
751 use std::time::Duration;
752
753 use super::*;
754
755 use rama_core::graceful::Shutdown;
756
757 use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
758
759 async fn run_default<S, T>(left: S, right: T) -> IoForwardOutcome
760 where
761 S: Io + Unpin,
762 T: Io + Unpin,
763 {
764 let svc = IoForwardService::default();
765 svc.serve(BridgeIo(left, right)).await.unwrap()
766 }
767
768 #[tokio::test]
769 async fn forward_basic_bidirectional_traffic() {
770 let (a_user, a_proxy) = duplex(64);
771 let (b_user, b_proxy) = duplex(64);
772
773 let svc_task = tokio::spawn(async move {
774 run_default(a_proxy, b_proxy).await;
775 });
776
777 let mut a = a_user;
778 let mut b = b_user;
779
780 a.write_all(b"hello").await.unwrap();
781 let mut buf = [0u8; 5];
782 b.read_exact(&mut buf).await.unwrap();
783 assert_eq!(&buf, b"hello");
784
785 b.write_all(b"world!").await.unwrap();
786 let mut buf = [0u8; 6];
787 a.read_exact(&mut buf).await.unwrap();
788 assert_eq!(&buf, b"world!");
789
790 drop(a);
792 drop(b);
793 svc_task.await.unwrap();
794 }
795
796 async fn shutdown_pair() -> (Shutdown, tokio::sync::oneshot::Sender<()>) {
797 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
798 let shutdown = Shutdown::new(async move {
799 _ = rx.await;
800 });
801 (shutdown, tx)
802 }
803
804 #[tokio::test]
805 async fn forward_shutdown_drops_idle_bridge() {
806 let (shutdown, trigger) = shutdown_pair().await;
807 let guard = shutdown.guard();
808 let svc = IoForwardService::new(Executor::graceful(guard));
809
810 let (_a_user, a_proxy) = duplex(64);
811 let (_b_user, b_proxy) = duplex(64);
812
813 let task = tokio::spawn(async move {
814 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
815 });
816
817 tokio::time::sleep(Duration::from_millis(10)).await;
818
819 let started = Instant::now();
820 trigger.send(()).unwrap();
821 tokio::time::timeout(Duration::from_secs(2), task)
822 .await
823 .expect("bridge did not unwind within 2s")
824 .unwrap();
825 let elapsed = started.elapsed();
826 assert!(
827 elapsed < Duration::from_millis(500),
828 "bridge took {elapsed:?} to unwind on shutdown",
829 );
830 drop(shutdown);
831 }
832
833 #[tokio::test]
834 async fn forward_shutdown_drops_active_bridge() {
835 let (shutdown, trigger) = shutdown_pair().await;
836 let guard = shutdown.guard();
837 let svc = IoForwardService::new(Executor::graceful(guard));
838
839 let (mut a_user, a_proxy) = duplex(64);
840 let (mut b_user, b_proxy) = duplex(64);
841
842 let task = tokio::spawn(async move {
843 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
844 });
845
846 a_user.write_all(b"hello").await.unwrap();
847 let mut buf = [0u8; 5];
848 b_user.read_exact(&mut buf).await.unwrap();
849 assert_eq!(&buf, b"hello");
850
851 let started = Instant::now();
852 trigger.send(()).unwrap();
853 tokio::time::timeout(Duration::from_secs(2), task)
854 .await
855 .expect("bridge did not unwind within 2s")
856 .unwrap();
857 let elapsed = started.elapsed();
858 assert!(
859 elapsed < Duration::from_millis(500),
860 "bridge took {elapsed:?} to unwind on shutdown",
861 );
862 drop(shutdown);
863 }
864
865 #[tokio::test]
866 async fn forward_idle_timeout_fires_when_no_progress() {
867 let svc = IoForwardService::default().with_idle_timeout(Duration::from_millis(100));
868
869 let (_a_user, a_proxy) = duplex(64);
870 let (_b_user, b_proxy) = duplex(64);
871
872 let started = Instant::now();
873 let outcome = tokio::time::timeout(
874 Duration::from_secs(2),
875 svc.serve(BridgeIo(a_proxy, b_proxy)),
876 )
877 .await
878 .expect("idle bridge did not unwind within 2s")
879 .unwrap();
880 assert_eq!(outcome.reason(), BridgeCloseReason::IdleTimeout);
881 let elapsed = started.elapsed();
882 assert!(
883 elapsed >= Duration::from_millis(80),
884 "idle bridge unwound too early: {elapsed:?}",
885 );
886 assert!(
887 elapsed < Duration::from_millis(800),
888 "idle bridge unwound too late: {elapsed:?}",
889 );
890 }
891
892 #[tokio::test(start_paused = true)]
893 async fn forward_first_byte_timeout_fires_when_upstream_silent() {
894 let svc = IoForwardService::default().with_first_byte_timeout(Duration::from_millis(100));
895
896 let (mut a_user, a_proxy) = duplex(64);
897 let (_b_user, b_proxy) = duplex(64);
898
899 a_user.write_all(b"hello").await.unwrap();
900
901 let started = tokio::time::Instant::now();
902 tokio::time::timeout(
903 Duration::from_secs(5),
904 svc.serve(BridgeIo(a_proxy, b_proxy)),
905 )
906 .await
907 .expect("silent-upstream bridge did not unwind")
908 .unwrap();
909
910 assert_eq!(
911 started.elapsed(),
912 Duration::from_millis(100),
913 "first-byte timeout should fire exactly at its deadline",
914 );
915 }
916
917 #[tokio::test(start_paused = true)]
918 async fn forward_first_byte_survives_when_upstream_speaks() {
919 let svc = IoForwardService::default()
920 .with_first_byte_timeout(Duration::from_millis(100))
921 .with_first_byte_timeout_start(FirstByteTimeoutStart::BridgeOpen)
922 .with_idle_timeout(Duration::from_millis(200));
923
924 let (mut a_user, a_proxy) = duplex(64);
925 let (mut b_user, b_proxy) = duplex(64);
926
927 let task = tokio::spawn(async move {
928 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
929 });
930
931 let started = tokio::time::Instant::now();
932
933 b_user.write_all(b"x").await.unwrap();
934 let mut buf = [0u8; 1];
935 a_user.read_exact(&mut buf).await.unwrap();
936
937 tokio::time::timeout(Duration::from_secs(5), task)
938 .await
939 .expect("bridge did not unwind")
940 .unwrap();
941 assert!(
942 started.elapsed() > Duration::from_millis(100),
943 "bridge closed inside the first-byte window ({:?}); the upstream byte should have disarmed it",
944 started.elapsed(),
945 );
946 }
947
948 #[tokio::test(start_paused = true)]
949 async fn forward_first_byte_disarmed_on_upstream_eof_before_byte() {
950 let svc = IoForwardService::default()
955 .with_first_byte_timeout(Duration::from_millis(10))
956 .with_first_byte_timeout_start(FirstByteTimeoutStart::BridgeOpen)
957 .with_shutdown_grace(Duration::from_millis(100));
958
959 struct PendingShutdownIo {
960 inner: tokio::io::DuplexStream,
961 }
962
963 impl tokio::io::AsyncRead for PendingShutdownIo {
964 fn poll_read(
965 mut self: std::pin::Pin<&mut Self>,
966 cx: &mut std::task::Context<'_>,
967 buf: &mut tokio::io::ReadBuf<'_>,
968 ) -> std::task::Poll<std::io::Result<()>> {
969 tokio::io::AsyncRead::poll_read(std::pin::Pin::new(&mut self.inner), cx, buf)
970 }
971 }
972
973 impl tokio::io::AsyncWrite for PendingShutdownIo {
974 fn poll_write(
975 mut self: std::pin::Pin<&mut Self>,
976 cx: &mut std::task::Context<'_>,
977 buf: &[u8],
978 ) -> std::task::Poll<std::io::Result<usize>> {
979 tokio::io::AsyncWrite::poll_write(std::pin::Pin::new(&mut self.inner), cx, buf)
980 }
981
982 fn poll_flush(
983 mut self: std::pin::Pin<&mut Self>,
984 cx: &mut std::task::Context<'_>,
985 ) -> std::task::Poll<std::io::Result<()>> {
986 tokio::io::AsyncWrite::poll_flush(std::pin::Pin::new(&mut self.inner), cx)
987 }
988
989 fn poll_shutdown(
990 self: std::pin::Pin<&mut Self>,
991 _: &mut std::task::Context<'_>,
992 ) -> std::task::Poll<std::io::Result<()>> {
993 std::task::Poll::Pending
994 }
995 }
996
997 let (a_user, a_proxy) = duplex(64);
998 let a_proxy = PendingShutdownIo { inner: a_proxy };
999 let (b_user, b_proxy) = duplex(64);
1000
1001 let task = tokio::spawn(async move {
1002 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
1003 });
1004
1005 drop(b_user);
1007
1008 tokio::time::sleep(Duration::from_millis(50)).await;
1013 assert!(
1014 !task.is_finished(),
1015 "first-byte timer fired while graceful shutdown followed a clean upstream EOF",
1016 );
1017
1018 drop(a_user);
1020 tokio::time::timeout(Duration::from_secs(5), task)
1021 .await
1022 .expect("bridge did not unwind after client EOF")
1023 .unwrap();
1024 }
1025
1026 #[tokio::test(start_paused = true)]
1027 async fn forward_first_byte_survives_client_backpressure() {
1028 let svc = IoForwardService::default()
1029 .with_first_byte_timeout(Duration::from_millis(100))
1030 .with_first_byte_timeout_start(FirstByteTimeoutStart::BridgeOpen)
1031 .with_idle_timeout(Duration::from_millis(200));
1032
1033 let (_a_user, a_proxy) = duplex(1);
1037 let (mut b_user, b_proxy) = duplex(64);
1038
1039 let task = tokio::spawn(async move {
1040 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
1041 });
1042
1043 let started = tokio::time::Instant::now();
1044 b_user.write_all(b"response").await.unwrap();
1045
1046 tokio::time::timeout(Duration::from_secs(5), task)
1047 .await
1048 .expect("bridge did not unwind")
1049 .unwrap();
1050 assert_eq!(
1051 started.elapsed(),
1052 Duration::from_millis(200),
1053 "upstream response should disarm first-byte timeout even when the client is backpressured",
1054 );
1055 }
1056
1057 #[tokio::test(start_paused = true)]
1058 async fn forward_first_byte_client_start_anchors_on_client_byte() {
1059 let svc = IoForwardService::default().with_first_byte_timeout(Duration::from_millis(100));
1063
1064 let (mut a_user, a_proxy) = duplex(64);
1065 let (_b_user, b_proxy) = duplex(64);
1066
1067 let task = tokio::spawn(async move {
1068 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
1069 });
1070
1071 tokio::time::sleep(Duration::from_millis(300)).await;
1075 assert!(
1076 !task.is_finished(),
1077 "first-byte window started before the client sent anything",
1078 );
1079
1080 let spoke_at = tokio::time::Instant::now();
1083 a_user.write_all(b"hello").await.unwrap();
1084
1085 tokio::time::timeout(Duration::from_secs(5), task)
1086 .await
1087 .expect("silent-upstream bridge did not unwind")
1088 .unwrap();
1089 assert_eq!(
1090 spoke_at.elapsed(),
1091 Duration::from_millis(100),
1092 "first-byte window should be measured from the client's first byte",
1093 );
1094 }
1095
1096 #[tokio::test]
1097 async fn forward_idle_timeout_resets_on_progress() {
1098 let svc = IoForwardService::default().with_idle_timeout(Duration::from_millis(150));
1099
1100 let (mut a_user, a_proxy) = duplex(64);
1101 let (mut b_user, b_proxy) = duplex(64);
1102
1103 let task = tokio::spawn(async move {
1104 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
1105 });
1106
1107 for _ in 0..8 {
1110 a_user.write_all(b"x").await.unwrap();
1111 let mut buf = [0u8; 1];
1112 b_user.read_exact(&mut buf).await.unwrap();
1113 tokio::time::sleep(Duration::from_millis(50)).await;
1114 }
1115
1116 drop(a_user);
1117 drop(b_user);
1118 tokio::time::timeout(Duration::from_secs(2), task)
1119 .await
1120 .expect("bridge did not unwind on EOF within 2s")
1121 .unwrap();
1122 }
1123
1124 #[tokio::test]
1125 async fn forward_outcome_reports_reason_and_byte_counts() {
1126 let (mut a_user, a_proxy) = duplex(64);
1127 let (mut b_user, b_proxy) = duplex(64);
1128
1129 let task = tokio::spawn(async move { run_default(a_proxy, b_proxy).await });
1130
1131 a_user.write_all(b"abc").await.unwrap();
1132 let mut buf = [0u8; 3];
1133 b_user.read_exact(&mut buf).await.unwrap();
1134 b_user.write_all(b"defgh").await.unwrap();
1135 let mut buf = [0u8; 5];
1136 a_user.read_exact(&mut buf).await.unwrap();
1137
1138 drop(a_user);
1139 drop(b_user);
1140 let outcome = task.await.unwrap();
1141
1142 assert!(
1143 matches!(
1144 outcome.reason(),
1145 BridgeCloseReason::PeerEofLeft | BridgeCloseReason::PeerEofRight
1146 ),
1147 "unexpected reason: {:?}",
1148 outcome.reason(),
1149 );
1150 assert_eq!(outcome.bytes_l_to_r(), 3);
1151 assert_eq!(outcome.bytes_r_to_l(), 5);
1152 assert_eq!(outcome.bytes_total(), 8);
1153 assert!(outcome.fatal_error().is_none());
1154 }
1155
1156 #[tokio::test]
1157 async fn forward_default_executor_means_no_shutdown_observation() {
1158 let svc = IoForwardService::default();
1161
1162 let (a_user, a_proxy) = duplex(64);
1163 let (b_user, b_proxy) = duplex(64);
1164
1165 let task = tokio::spawn(async move {
1166 svc.serve(BridgeIo(a_proxy, b_proxy)).await.unwrap();
1167 });
1168
1169 tokio::time::sleep(Duration::from_millis(100)).await;
1170 assert!(!task.is_finished(), "bridge ended without an EOF signal");
1171
1172 drop(a_user);
1173 drop(b_user);
1174 tokio::time::timeout(Duration::from_secs(2), task)
1175 .await
1176 .expect("bridge did not unwind on EOF within 2s")
1177 .unwrap();
1178 }
1179
1180 #[tokio::test]
1187 async fn copy_one_way_calls_shutdown_once_on_write_error() {
1188 use std::sync::atomic::AtomicUsize;
1189 use std::task::{Context, Poll};
1190
1191 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
1192
1193 struct ReadOnce {
1194 done: bool,
1195 }
1196 impl AsyncRead for ReadOnce {
1197 fn poll_read(
1198 mut self: std::pin::Pin<&mut Self>,
1199 _: &mut Context<'_>,
1200 buf: &mut ReadBuf<'_>,
1201 ) -> Poll<std::io::Result<()>> {
1202 if self.done {
1203 return Poll::Ready(Ok(()));
1204 }
1205 self.done = true;
1206 buf.put_slice(b"hi");
1207 Poll::Ready(Ok(()))
1208 }
1209 }
1210
1211 struct CountingWriter {
1212 shutdown_calls: Arc<AtomicUsize>,
1213 fail_write: bool,
1214 }
1215 impl AsyncWrite for CountingWriter {
1216 fn poll_write(
1217 self: std::pin::Pin<&mut Self>,
1218 _: &mut Context<'_>,
1219 buf: &[u8],
1220 ) -> Poll<std::io::Result<usize>> {
1221 if self.fail_write {
1222 Poll::Ready(Err(std::io::Error::new(
1223 std::io::ErrorKind::BrokenPipe,
1224 "test",
1225 )))
1226 } else {
1227 Poll::Ready(Ok(buf.len()))
1228 }
1229 }
1230 fn poll_flush(
1231 self: std::pin::Pin<&mut Self>,
1232 _: &mut Context<'_>,
1233 ) -> Poll<std::io::Result<()>> {
1234 Poll::Ready(Ok(()))
1235 }
1236 fn poll_shutdown(
1237 self: std::pin::Pin<&mut Self>,
1238 _: &mut Context<'_>,
1239 ) -> Poll<std::io::Result<()>> {
1240 self.shutdown_calls.fetch_add(1, Ordering::Relaxed);
1241 Poll::Ready(Ok(()))
1242 }
1243 }
1244
1245 let shutdown_calls = Arc::new(AtomicUsize::new(0));
1246 let mut reader = ReadOnce { done: false };
1247 let mut writer = CountingWriter {
1248 shutdown_calls: shutdown_calls.clone(),
1249 fail_write: true,
1250 };
1251 let bytes = Arc::new(AtomicU64::new(0));
1252 let progress = Arc::new(AtomicU64::new(0));
1253 let write_side_shut = Arc::new(AtomicBool::new(false));
1254 let res = copy_one_way(
1255 &mut reader,
1256 &mut writer,
1257 bytes,
1258 progress,
1259 64,
1260 Duration::from_millis(50),
1261 write_side_shut.clone(),
1262 None,
1263 None,
1264 None,
1265 )
1266 .await;
1267 assert!(res.is_err(), "expected write error to propagate");
1268 assert_eq!(
1269 shutdown_calls.load(Ordering::Relaxed),
1270 1,
1271 "shutdown must be called exactly once even on the write-error path",
1272 );
1273 assert!(
1274 write_side_shut.load(Ordering::Acquire),
1275 "write_side_shut flag must be set so run_bridge skips a duplicate shutdown",
1276 );
1277 }
1278
1279 struct ScriptedIo {
1283 read_err: Option<std::io::ErrorKind>,
1284 errored: bool,
1285 }
1286
1287 impl ScriptedIo {
1288 fn erroring(kind: std::io::ErrorKind) -> Self {
1289 Self {
1290 read_err: Some(kind),
1291 errored: false,
1292 }
1293 }
1294
1295 fn pending() -> Self {
1296 Self {
1297 read_err: None,
1298 errored: false,
1299 }
1300 }
1301 }
1302
1303 impl tokio::io::AsyncRead for ScriptedIo {
1304 fn poll_read(
1305 mut self: std::pin::Pin<&mut Self>,
1306 _: &mut std::task::Context<'_>,
1307 _buf: &mut tokio::io::ReadBuf<'_>,
1308 ) -> std::task::Poll<std::io::Result<()>> {
1309 match self.read_err {
1310 Some(kind) if !self.errored => {
1311 self.errored = true;
1312 std::task::Poll::Ready(Err(std::io::Error::new(kind, "scripted")))
1313 }
1314 _ => std::task::Poll::Pending,
1317 }
1318 }
1319 }
1320
1321 impl tokio::io::AsyncWrite for ScriptedIo {
1322 fn poll_write(
1323 self: std::pin::Pin<&mut Self>,
1324 _: &mut std::task::Context<'_>,
1325 buf: &[u8],
1326 ) -> std::task::Poll<std::io::Result<usize>> {
1327 std::task::Poll::Ready(Ok(buf.len()))
1328 }
1329
1330 fn poll_flush(
1331 self: std::pin::Pin<&mut Self>,
1332 _: &mut std::task::Context<'_>,
1333 ) -> std::task::Poll<std::io::Result<()>> {
1334 std::task::Poll::Ready(Ok(()))
1335 }
1336
1337 fn poll_shutdown(
1338 self: std::pin::Pin<&mut Self>,
1339 _: &mut std::task::Context<'_>,
1340 ) -> std::task::Poll<std::io::Result<()>> {
1341 std::task::Poll::Ready(Ok(()))
1342 }
1343 }
1344
1345 #[tokio::test]
1346 async fn forward_genuine_error_surfaces_as_err_outcome() {
1347 let left = ScriptedIo::erroring(std::io::ErrorKind::InvalidData);
1349 let right = ScriptedIo::pending();
1350
1351 let svc = IoForwardService::default();
1352 let err = svc
1353 .serve(BridgeIo(left, right))
1354 .await
1355 .expect_err("genuine (non-connection) error must surface as Err");
1356
1357 assert!(err.fatal_error().is_some());
1358 assert!(
1359 matches!(
1360 err.outcome().reason(),
1361 BridgeCloseReason::ReadErrorLeft | BridgeCloseReason::WriteErrorRight
1362 ),
1363 "unexpected reason: {:?}",
1364 err.reason(),
1365 );
1366 }
1367
1368 #[tokio::test]
1369 async fn forward_connection_error_stays_ok_but_is_exposed() {
1370 let left = ScriptedIo::erroring(std::io::ErrorKind::ConnectionReset);
1373 let right = ScriptedIo::pending();
1374
1375 let svc = IoForwardService::default();
1376 let outcome = svc
1377 .serve(BridgeIo(left, right))
1378 .await
1379 .expect("connection reset must stay Ok");
1380
1381 assert_eq!(outcome.reason(), BridgeCloseReason::ReadErrorLeft);
1382 assert!(outcome.fatal_error().is_some());
1383 }
1384}