1use std::future::Future;
30use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
31use std::sync::{Arc, Mutex, PoisonError};
32use std::time::Duration;
33
34use tokio::net::TcpListener;
35use tokio::sync::{oneshot, watch};
36use tokio::task::JoinSet;
37
38use microvms_app::error::{Error, ErrorKind};
39use microvms_app::identity::TunnelIdentity;
40use microvms_app::session::Session;
41use microvms_app::session::proxy::ProxyAuth;
42
43use super::forward::{ForwardClient, ForwardEvent, ForwardSpec, serve_connection_via};
44use super::tunnel::{TunnelEnd, relay_connection_inner};
45
46#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub struct ServeLimits {
49 pub max_connections: Option<u32>,
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum StopReason {
56 Requested,
58 Limit,
60 ListenerFailed(String),
63}
64
65impl StopReason {
66 pub fn was_requested(&self) -> bool {
69 matches!(self, StopReason::Requested)
70 }
71}
72
73#[derive(Clone)]
75pub struct TunnelTarget {
76 pub endpoint: String,
77 pub agent_token: String,
78 pub auth: Option<Arc<ProxyAuth>>,
80 pub guest_port: u16,
81 pub identity: Option<TunnelIdentity>,
83}
84
85impl TunnelTarget {
86 pub fn for_session(
88 session: &Session,
89 guest_port: u16,
90 identity: Option<TunnelIdentity>,
91 ) -> Self {
92 Self {
93 endpoint: session.endpoint().to_string(),
94 agent_token: session.agent_token().to_string(),
95 auth: session.proxy_auth().cloned(),
96 guest_port,
97 identity,
98 }
99 }
100}
101
102#[derive(Debug)]
104pub struct TunnelConnection {
105 pub peer: SocketAddr,
106 pub end: Result<TunnelEnd, Error>,
107}
108
109#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct TunnelReport {
112 pub served: u32,
114 pub refused: u32,
116 pub truncated: u32,
118 pub unproven: u32,
120 pub proxy_token_mints: u64,
122 pub stopped: StopReason,
123}
124
125impl TunnelReport {
126 fn count(&mut self, end: &Result<TunnelEnd, Error>) {
128 match end {
129 Ok(TunnelEnd::Closed) => {}
130 Ok(TunnelEnd::ClosedUnproven) => self.unproven += 1,
131 Ok(TunnelEnd::Truncated { .. }) => self.truncated += 1,
132 Ok(TunnelEnd::Refused { .. }) | Err(_) => self.refused += 1,
133 }
134 }
135}
136
137pub async fn serve_tunnel<F>(
142 listener: TcpListener,
143 target: TunnelTarget,
144 limits: ServeLimits,
145 stop: impl Future<Output = ()>,
146 on_end: F,
147) -> TunnelReport
148where
149 F: FnMut(TunnelConnection),
150{
151 tunnel_loop(
152 listener,
153 target,
154 limits,
155 stop,
156 std::future::pending(),
157 on_end,
158 )
159 .await
160}
161
162async fn tunnel_loop<F>(
164 listener: TcpListener,
165 target: TunnelTarget,
166 limits: ServeLimits,
167 stop: impl Future<Output = ()>,
168 cut: impl Future<Output = ()>,
169 mut on_end: F,
170) -> TunnelReport
171where
172 F: FnMut(TunnelConnection),
173{
174 let target = Arc::new(target);
175 let mut report = TunnelReport {
176 served: 0,
177 refused: 0,
178 truncated: 0,
179 unproven: 0,
180 proxy_token_mints: 0,
181 stopped: StopReason::Requested,
182 };
183 let (stopped, served) = serve(
184 &listener,
185 limits,
186 stop,
187 cut,
188 |mut stream, _| {
189 let target = Arc::clone(&target);
190 async move {
191 let result = relay_connection_inner(
192 &mut stream,
193 &target.endpoint,
194 target.guest_port,
195 &target.agent_token,
196 target.auth.as_ref(),
197 target.identity.as_ref(),
198 )
199 .await;
200 if target.identity.is_some()
204 && matches!(
205 result,
206 Err(_) | Ok(TunnelEnd::Truncated { .. } | TunnelEnd::Refused { .. })
207 )
208 {
209 stream.set_zero_linger().map_err(|error| {
210 Error::new(
211 ErrorKind::Unexpected,
212 format!("could not reset the local tunnel connection: {error}"),
213 )
214 })?;
215 }
216 result
217 }
218 },
219 |peer, end| {
220 report.count(&end);
221 on_end(TunnelConnection { peer, end });
222 },
223 )
224 .await;
225 report.served = served;
226 report.stopped = stopped;
227 report.proxy_token_mints = target.auth.as_ref().map_or(0, |auth| auth.mint_count());
228 report
229}
230
231#[derive(Debug)]
233pub enum ForwardNotice {
234 Event {
236 peer: SocketAddr,
237 event: ForwardEvent,
238 },
239 Failed { peer: SocketAddr, error: Error },
241}
242
243#[derive(Clone, Debug, Eq, PartialEq)]
245pub struct ForwardReport {
246 pub served: u32,
248 pub refused: u32,
250 pub upgrades: u32,
252 pub proxy_token_mints: u64,
254 pub stopped: StopReason,
255}
256
257impl ForwardReport {
258 fn count(&mut self, event: &ForwardEvent) {
260 match event {
261 ForwardEvent::Refused { .. } => self.refused += 1,
262 ForwardEvent::Forwarded { upgraded: true, .. } => self.upgrades += 1,
263 _ => {}
264 }
265 }
266}
267
268pub async fn serve_forward<F>(
275 listener: TcpListener,
276 spec: ForwardSpec,
277 auth: Option<Arc<ProxyAuth>>,
278 limits: ServeLimits,
279 stop: impl Future<Output = ()>,
280 on_notice: F,
281) -> Result<ForwardReport, Error>
282where
283 F: FnMut(ForwardNotice),
284{
285 forward_loop(
286 listener,
287 spec,
288 auth,
289 limits,
290 stop,
291 std::future::pending(),
292 on_notice,
293 )
294 .await
295}
296
297async fn forward_loop<F>(
299 listener: TcpListener,
300 spec: ForwardSpec,
301 auth: Option<Arc<ProxyAuth>>,
302 limits: ServeLimits,
303 stop: impl Future<Output = ()>,
304 cut: impl Future<Output = ()>,
305 mut on_notice: F,
306) -> Result<ForwardReport, Error>
307where
308 F: FnMut(ForwardNotice),
309{
310 let client = Arc::new(ForwardClient::new()?);
313 let mut report = ForwardReport {
314 served: 0,
315 refused: 0,
316 upgrades: 0,
317 proxy_token_mints: 0,
318 stopped: StopReason::Requested,
319 };
320 let (stopped, served) = serve(
321 &listener,
322 limits,
323 stop,
324 cut,
325 |stream, _| {
326 let (mut spec, auth, client) = (spec.clone(), auth.clone(), Arc::clone(&client));
327 async move {
328 spec.bind = stream.local_addr().map_err(|error| {
331 Error::new(
332 ErrorKind::Unexpected,
333 format!("reading the local forward address failed: {error}"),
334 )
335 })?;
336 let mut events = Vec::new();
337 let outcome =
338 serve_connection_via(stream, &spec, auth.as_ref(), &client, |event| {
339 events.push(event);
340 })
341 .await;
342 outcome.map(|()| events)
343 }
344 },
345 |peer, done| {
346 let (events, failure) = match done {
347 Ok(events) => (events, None),
348 Err(error) => (Vec::new(), Some(error)),
349 };
350 for event in events {
351 report.count(&event);
352 on_notice(ForwardNotice::Event { peer, event });
353 }
354 if let Some(error) = failure {
355 on_notice(ForwardNotice::Failed { peer, error });
356 }
357 },
358 )
359 .await;
360 report.served = served;
361 report.stopped = stopped;
362 report.proxy_token_mints = auth.as_ref().map_or(0, |auth| auth.mint_count());
363 Ok(report)
364}
365
366pub const DEFAULT_BIND: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
371
372pub fn bind_address(bind: Option<&str>) -> Result<SocketAddr, Error> {
374 match bind {
375 None => Ok(DEFAULT_BIND),
376 Some(bind) => bind.parse().map_err(|err| {
377 Error::new(
378 ErrorKind::InvalidArg,
379 format!("{bind:?} is not a local address to listen on, such as 127.0.0.1:0: {err}"),
380 )
381 }),
382 }
383}
384
385#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum EndKind {
388 Refused,
390 Truncated,
392 Unproven,
394 Failed,
396}
397
398impl EndKind {
399 pub fn as_str(self) -> &'static str {
401 match self {
402 EndKind::Refused => "refused",
403 EndKind::Truncated => "truncated",
404 EndKind::Unproven => "unproven",
405 EndKind::Failed => "failed",
406 }
407 }
408}
409
410#[derive(Clone, Debug, Eq, PartialEq)]
413pub struct ConnectionEnd {
414 pub peer: SocketAddr,
415 pub kind: EndKind,
416 pub code: Option<u16>,
418 pub detail: String,
420}
421
422impl ConnectionEnd {
423 pub fn of_tunnel(connection: &TunnelConnection) -> Option<Self> {
425 let (kind, code, detail) = match &connection.end {
426 Ok(TunnelEnd::Closed) => return None,
427 Ok(TunnelEnd::ClosedUnproven) => (EndKind::Unproven, None, String::new()),
428 Ok(TunnelEnd::Truncated { code }) => (EndKind::Truncated, *code, String::new()),
429 Ok(TunnelEnd::Refused { code, reason }) => {
430 (EndKind::Refused, Some(*code), reason.clone())
431 }
432 Err(error) => (EndKind::Failed, None, error.to_string()),
433 };
434 Some(Self {
435 peer: connection.peer,
436 kind,
437 code,
438 detail,
439 })
440 }
441
442 pub fn of_forward(notice: &ForwardNotice) -> Option<Self> {
444 match notice {
445 ForwardNotice::Event {
446 peer,
447 event:
448 ForwardEvent::Refused {
449 status,
450 explanation,
451 },
452 } => Some(Self {
453 peer: *peer,
454 kind: EndKind::Refused,
455 code: Some(*status),
456 detail: explanation.clone(),
457 }),
458 ForwardNotice::Event { .. } => None,
459 ForwardNotice::Failed { peer, error } => Some(Self {
460 peer: *peer,
461 kind: EndKind::Failed,
462 code: None,
463 detail: error.to_string(),
464 }),
465 }
466 }
467}
468
469#[derive(Clone, Debug, Eq, PartialEq)]
471pub struct TunnelSummary {
472 pub report: TunnelReport,
473 pub ended: Vec<ConnectionEnd>,
474}
475
476#[derive(Clone, Debug, Eq, PartialEq)]
479pub struct ForwardSummary {
480 pub report: ForwardReport,
481 pub ended: Vec<ConnectionEnd>,
482}
483
484type Outcome<S> = Result<S, (ErrorKind, String)>;
485
486pub struct Serving<S> {
491 local: SocketAddr,
492 stop: Mutex<Option<oneshot::Sender<()>>>,
493 cut: Mutex<Option<oneshot::Sender<()>>>,
494 outcome: Arc<Mutex<Option<Outcome<S>>>>,
495 done: watch::Receiver<bool>,
496}
497
498impl<S: Clone + Send + 'static> Serving<S> {
499 fn spawn<Fut>(
502 local: SocketAddr,
503 serving: impl FnOnce(oneshot::Receiver<()>, oneshot::Receiver<()>) -> Fut,
504 ) -> Self
505 where
506 Fut: Future<Output = Result<S, Error>> + Send + 'static,
507 {
508 let (stop_tx, stop_rx) = oneshot::channel();
509 let (cut_tx, cut_rx) = oneshot::channel();
510 let (done_tx, done_rx) = watch::channel(false);
511 let outcome = Arc::new(Mutex::new(None));
512 let written = Arc::clone(&outcome);
513 let serving = serving(stop_rx, cut_rx);
514 tokio::spawn(async move {
515 let result = serving
516 .await
517 .map_err(|error| (error.kind(), error.to_string()));
518 *written.lock().unwrap_or_else(PoisonError::into_inner) = Some(result);
519 done_tx.send_replace(true);
520 });
521 Self {
522 local,
523 stop: Mutex::new(Some(stop_tx)),
524 cut: Mutex::new(Some(cut_tx)),
525 outcome,
526 done: done_rx,
527 }
528 }
529
530 pub fn local_address(&self) -> SocketAddr {
532 self.local
533 }
534
535 pub fn is_running(&self) -> bool {
537 !*self.done.borrow()
538 }
539
540 pub fn request_stop(&self) {
543 signal(&self.stop);
544 }
545
546 pub async fn stop(&self, grace: Option<Duration>) -> Result<S, Error> {
552 self.request_stop();
553 if let Some(grace) = grace {
554 if let Ok(outcome) = tokio::time::timeout(grace, self.finished()).await {
555 return outcome;
556 }
557 signal(&self.cut);
558 }
559 self.finished().await
560 }
561
562 pub async fn finished(&self) -> Result<S, Error> {
564 let mut done = self.done.clone();
565 let _ = done.wait_for(|finished| *finished).await;
567 self.outcome
568 .lock()
569 .unwrap_or_else(PoisonError::into_inner)
570 .clone()
571 .expect("the loop records its outcome before signalling done")
572 .map_err(|(kind, message)| Error::new(kind, message))
573 }
574}
575
576fn signal(sender: &Mutex<Option<oneshot::Sender<()>>>) {
578 if let Some(sender) = sender.lock().unwrap_or_else(PoisonError::into_inner).take() {
579 let _ = sender.send(());
580 }
581}
582
583async fn signalled(signal: oneshot::Receiver<()>) {
585 let _ = signal.await;
586}
587
588pub async fn start_tunnel(
591 bind: SocketAddr,
592 target: TunnelTarget,
593 limits: ServeLimits,
594) -> Result<Serving<TunnelSummary>, Error> {
595 let listener =
596 super::forward::bind(&ForwardSpec::new(bind, target.guest_port, &target.endpoint)).await?;
597 let local = local_address(&listener)?;
598 Ok(Serving::spawn(local, move |stop, cut| async move {
599 let mut ended = Vec::new();
600 let report = tunnel_loop(
601 listener,
602 target,
603 limits,
604 signalled(stop),
605 signalled(cut),
606 |connection| ended.extend(ConnectionEnd::of_tunnel(&connection)),
607 )
608 .await;
609 Ok(TunnelSummary { report, ended })
610 }))
611}
612
613pub async fn start_forward(
616 bind: SocketAddr,
617 endpoint: &str,
618 guest_port: u16,
619 auth: Option<Arc<ProxyAuth>>,
620 limits: ServeLimits,
621) -> Result<Serving<ForwardSummary>, Error> {
622 let spec = ForwardSpec::new(bind, guest_port, endpoint);
623 let listener = super::forward::bind(&spec).await?;
624 let local = local_address(&listener)?;
625 Ok(Serving::spawn(local, move |stop, cut| async move {
626 let mut ended = Vec::new();
627 let report = forward_loop(
628 listener,
629 spec,
630 auth,
631 limits,
632 signalled(stop),
633 signalled(cut),
634 |notice| ended.extend(ConnectionEnd::of_forward(¬ice)),
635 )
636 .await?;
637 Ok(ForwardSummary { report, ended })
638 }))
639}
640
641fn local_address(listener: &TcpListener) -> Result<SocketAddr, Error> {
642 listener.local_addr().map_err(|err| {
643 Error::new(
644 ErrorKind::Unexpected,
645 format!("the listener has no address: {err}"),
646 )
647 })
648}
649
650async fn serve<T, Fut>(
657 listener: &TcpListener,
658 limits: ServeLimits,
659 stop: impl Future<Output = ()>,
660 cut: impl Future<Output = ()>,
661 mut spawn: impl FnMut(tokio::net::TcpStream, SocketAddr) -> Fut,
662 mut ended: impl FnMut(SocketAddr, Result<T, Error>),
663) -> (StopReason, u32)
664where
665 T: Send + 'static,
666 Fut: Future<Output = Result<T, Error>> + Send + 'static,
667{
668 let mut stop = std::pin::pin!(stop);
669 let mut tasks = JoinSet::new();
670 let mut peers = Peers::new();
672 type Peers = std::collections::HashMap<tokio::task::Id, SocketAddr>;
673 let mut finish =
674 |peers: &mut Peers,
675 joined: Result<(tokio::task::Id, Result<T, Error>), tokio::task::JoinError>| {
676 let (id, result) = match joined {
677 Ok((id, result)) => (id, result),
678 Err(error) if error.is_cancelled() => (
679 error.id(),
680 Err(Error::new(
681 ErrorKind::Unexpected,
682 "the connection was cut when its loop stopped",
683 )),
684 ),
685 Err(error) => (
686 error.id(),
687 Err(Error::new(
688 ErrorKind::Unexpected,
689 format!("the connection's task ended abnormally: {error}"),
690 )),
691 ),
692 };
693 let peer = peers
694 .remove(&id)
695 .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0)));
696 ended(peer, result);
697 };
698 let mut served = 0_u32;
699 let stopped = loop {
700 if limits.max_connections.is_some_and(|max| served >= max) {
701 break StopReason::Limit;
702 }
703 tokio::select! {
704 biased;
707 () = &mut stop => break StopReason::Requested,
708 Some(joined) = tasks.join_next_with_id(), if !tasks.is_empty() => finish(&mut peers, joined),
709 next = listener.accept() => match next {
710 Ok((stream, peer)) => {
711 served += 1;
712 let handle = tasks.spawn(spawn(stream, peer));
713 peers.insert(handle.id(), peer);
714 }
715 Err(error) => {
716 break StopReason::ListenerFailed(format!(
717 "the local listener stopped accepting: {error}"
718 ));
719 }
720 },
721 }
722 };
723 let mut cut = std::pin::pin!(cut);
725 let mut cutting = false;
726 loop {
727 tokio::select! {
728 joined = tasks.join_next_with_id() => match joined {
729 Some(joined) => finish(&mut peers, joined),
730 None => break,
731 },
732 () = &mut cut, if !cutting => {
733 cutting = true;
734 tasks.abort_all();
735 }
736 }
737 }
738 (stopped, served)
739}
740
741#[cfg(test)]
742mod tests {
743 use super::*;
744
745 fn tunnel_report() -> TunnelReport {
746 TunnelReport {
747 served: 0,
748 refused: 0,
749 truncated: 0,
750 unproven: 0,
751 proxy_token_mints: 0,
752 stopped: StopReason::Requested,
753 }
754 }
755
756 #[test]
758 fn each_tunnel_end_is_counted_once_in_its_column() {
759 let mut report = tunnel_report();
760 for end in [
761 Ok(TunnelEnd::Closed),
762 Ok(TunnelEnd::ClosedUnproven),
763 Ok(TunnelEnd::ClosedUnproven),
764 Ok(TunnelEnd::Truncated { code: None }),
765 Ok(TunnelEnd::Truncated { code: Some(1000) }),
766 Ok(TunnelEnd::Truncated { code: Some(1000) }),
767 Ok(TunnelEnd::Refused {
768 code: 4502,
769 reason: String::new(),
770 }),
771 Err(Error::new(ErrorKind::Unexpected, "a transport error")),
772 ] {
773 report.count(&end);
774 }
775 assert_eq!(
776 (report.refused, report.truncated, report.unproven),
777 (2, 3, 2),
778 "{report:?}"
779 );
780 }
781
782 #[test]
784 fn only_the_callers_stop_was_requested() {
785 assert!(StopReason::Requested.was_requested());
786 assert!(!StopReason::Limit.was_requested());
787 assert!(!StopReason::ListenerFailed("gone".to_string()).was_requested());
788 }
789
790 #[test]
792 fn each_forward_event_is_counted_once_in_its_column() {
793 let mut report = ForwardReport {
794 served: 0,
795 refused: 0,
796 upgrades: 0,
797 proxy_token_mints: 0,
798 stopped: StopReason::Requested,
799 };
800 for event in [
801 ForwardEvent::Refused {
802 status: 502,
803 explanation: String::new(),
804 },
805 ForwardEvent::Forwarded {
806 status: 101,
807 upgraded: true,
808 },
809 ForwardEvent::Forwarded {
810 status: 101,
811 upgraded: true,
812 },
813 ForwardEvent::Forwarded {
814 status: 200,
815 upgraded: false,
816 },
817 ForwardEvent::ConnectionError {
818 detail: String::new(),
819 },
820 ] {
821 report.count(&event);
822 }
823 assert_eq!((report.refused, report.upgrades), (1, 2), "{report:?}");
824 }
825
826 #[test]
829 fn each_end_but_a_clean_one_is_listed_with_its_kind() {
830 let peer: SocketAddr = "127.0.0.1:5000".parse().expect("an address");
831 let tunnel = |end| ConnectionEnd::of_tunnel(&TunnelConnection { peer, end });
832 let listed = |end: Option<ConnectionEnd>| {
833 end.map(|end| (end.peer, end.kind.as_str(), end.code, end.detail))
834 };
835 assert_eq!(tunnel(Ok(TunnelEnd::Closed)), None);
836 assert_eq!(
837 listed(tunnel(Ok(TunnelEnd::ClosedUnproven))),
838 Some((peer, "unproven", None, String::new()))
839 );
840 assert_eq!(
841 listed(tunnel(Ok(TunnelEnd::Truncated { code: Some(1006) }))),
842 Some((peer, "truncated", Some(1006), String::new()))
843 );
844 assert_eq!(
845 listed(tunnel(Ok(TunnelEnd::Refused {
846 code: 4502,
847 reason: "nothing is listening".into(),
848 }))),
849 Some((peer, "refused", Some(4502), "nothing is listening".into()))
850 );
851 let failed = listed(tunnel(Err(Error::new(ErrorKind::Unexpected, "a reset"))));
852 assert!(
853 matches!(&failed, Some((_, "failed", None, detail)) if detail.contains("a reset")),
854 "{failed:?}"
855 );
856
857 let forward = |event| ConnectionEnd::of_forward(&ForwardNotice::Event { peer, event });
858 assert_eq!(
859 listed(forward(ForwardEvent::Refused {
860 status: 403,
861 explanation: "out of scope".into(),
862 })),
863 Some((peer, "refused", Some(403), "out of scope".into()))
864 );
865 assert_eq!(
866 forward(ForwardEvent::Forwarded {
867 status: 200,
868 upgraded: false,
869 }),
870 None
871 );
872 let failed = listed(ConnectionEnd::of_forward(&ForwardNotice::Failed {
873 peer,
874 error: Error::new(ErrorKind::Unexpected, "a reset"),
875 }));
876 assert!(
877 matches!(&failed, Some((_, "failed", None, detail)) if detail.contains("a reset")),
878 "{failed:?}"
879 );
880 }
881
882 #[tokio::test]
885 async fn a_panicked_connection_and_a_cut_one_are_told_apart() {
886 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
887 let local = listener.local_addr().expect("bound");
888 let (cut, cut_rx) = oneshot::channel::<()>();
889 tokio::spawn(async move {
890 let _first = tokio::net::TcpStream::connect(local).await;
891 let _second = tokio::net::TcpStream::connect(local).await;
892 tokio::time::sleep(Duration::from_millis(200)).await;
893 let _ = cut.send(());
894 std::future::pending::<()>().await;
895 });
896 let mut accepted = 0;
897 let mut ended = Vec::new();
898 let serving = serve(
899 &listener,
900 ServeLimits {
901 max_connections: Some(2),
902 },
903 std::future::pending(),
904 signalled(cut_rx),
905 |stream, _| {
906 accepted += 1;
907 let panics = accepted == 1;
908 async move {
909 let _held = stream;
910 assert!(!panics, "the first connection's task panics");
911 std::future::pending::<Result<(), Error>>().await
912 }
913 },
914 |_, result| ended.push(result.expect_err("neither ends clean").to_string()),
915 );
916 let (stopped, served) = tokio::time::timeout(Duration::from_secs(10), serving)
917 .await
918 .expect("the cut ends the drain");
919 assert_eq!((stopped, served), (StopReason::Limit, 2));
920 assert_eq!(ended.len(), 2, "{ended:?}");
921 let panicked = ended.iter().filter(|end| end.contains("ended abnormally"));
922 let cut = ended.iter().filter(|end| end.contains("was cut"));
923 assert_eq!((panicked.count(), cut.count()), (1, 1), "{ended:?}");
924 }
925}