1pub mod complete;
33pub mod exec;
34pub mod files;
35pub mod http;
36pub mod keepalive;
37pub mod proxy;
38pub mod split;
39pub mod sse;
40mod upload;
41
42use std::sync::Arc;
43use std::time::Duration;
44
45pub use complete::{
46 ClientDeadline, CompletionOptions, CompletionPlan, DEFAULT_CLIENT_GRACE, KillAnswer,
47 NO_TIMEOUT_CEILING, OutputFlow, OutputSink,
48};
49pub use exec::{
50 DEFAULT_EXEC_WAIT, EndReason, ExecHandle, ExecResult, StreamEnd, StreamOptions,
51 TIMED_OUT_EXIT_CODE, exec_id_at,
52};
53pub use http::{ChunkSource, HttpBackend, HttpRequest, HttpResponse, OpenStream, SharedBackend};
54pub use keepalive::{KeepAwake, KeepAwakeEnd, KeepAwakeReport, KeepAwakeTask, RunningGate};
55pub use proxy::{
56 Clock, DEFAULT_AGENT_PORT, DEFAULT_REFRESH_AFTER, MAX_TOKEN_LIFETIME, PROXY_AUTH_HEADER,
57 PROXY_PORT_HEADER, ProxyAuth, ProxyToken, TokenMinter, WS_AUTH_SUBPROTOCOL_PREFIX,
58 WS_PORT_SUBPROTOCOL_PREFIX, WS_SUBPROTOCOL,
59};
60pub use split::{GapLog, GapPolicy, OutputGap, Split, SplitItem};
61pub use sse::{ExecEvent, Frame, SseParser};
62
63use crate::error::{Error, ErrorKind, WireKind};
64
65pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
67
68pub const DEFAULT_BOOTSTRAP_TIMEOUT: Duration = Duration::from_secs(120);
72
73#[deprecated(
75 note = "renamed DEFAULT_BOOTSTRAP_TIMEOUT: `sandbox::DEFAULT_READY_TIMEOUT` shared this name \
76 for a different wait, the one for RUNNING"
77)]
78pub const DEFAULT_READY_TIMEOUT: Duration = DEFAULT_BOOTSTRAP_TIMEOUT;
79
80const READY_POLL_INTERVAL: Duration = Duration::from_secs(2);
82
83pub(crate) const ROOT_UID: u32 = 0;
89
90pub struct Transport {
95 backend: http::SharedBackend,
96 agent_token: String,
97 proxy: Option<Arc<ProxyAuth>>,
102 timeout: Duration,
103}
104
105impl Transport {
106 pub fn proxy(&self) -> Option<&Arc<ProxyAuth>> {
108 self.proxy.as_ref()
109 }
110
111 async fn headers(&self, token: Option<Option<&str>>) -> Result<Vec<(String, String)>, Error> {
120 let mut headers = match &self.proxy {
123 Some(proxy) => proxy.headers().await?,
124 None => Vec::new(),
125 };
126 let bearer = match token {
127 Some(explicit) => explicit,
128 None => Some(self.agent_token.as_str()),
129 };
130 if let Some(bearer) = bearer {
131 headers.push(("Authorization".to_string(), format!("Bearer {bearer}")));
132 }
133 Ok(headers)
134 }
135
136 async fn request(&self, request: HttpRequest) -> Result<HttpResponse, Error> {
138 let timeout = request.timeout.unwrap_or(self.timeout);
139 let method = request.method;
140 let path = request.path.clone();
141 tokio::time::timeout(timeout, self.request_inner(request))
142 .await
143 .map_err(|_| Error::wire(WireKind::Transport,
144 format!("{method} {path} exceeded its request timeout (including proxy authentication)")))?
145 }
146
147 async fn request_inner(&self, mut request: HttpRequest) -> Result<HttpResponse, Error> {
148 let mut headers = self.headers(request_token(&request)).await?;
157 request.headers.retain(|(name, _)| name != TOKEN_INTENT);
158 headers.append(&mut request.headers);
159 request.headers = headers;
160 if request.timeout.is_none() {
161 request.timeout = Some(self.timeout);
162 }
163 self.backend.send(request).await
164 }
165
166 async fn send(&self, request: HttpRequest) -> Result<HttpResponse, Error> {
168 let (method, path) = (request.method, request.path.clone());
169 let response = self.request(request).await?;
170 response.error_for_status(method, &path)?;
171 Ok(response)
172 }
173
174 async fn send_json<T: serde::de::DeserializeOwned>(
176 &self,
177 request: HttpRequest,
178 ) -> Result<T, Error> {
179 let (method, path) = (request.method, request.path.clone());
180 let response = self.send(request).await?;
181 serde_json::from_slice(&response.body).map_err(|err| {
182 Error::wire(
186 WireKind::ProtocolError,
187 format!(
188 "{method} {path} answered a body this client cannot read: {err}; \
189 check /v1/schema for the daemon's protocol version"
190 ),
191 )
192 .with_source(err)
193 })
194 }
195}
196
197const TOKEN_INTENT: &str = "x-microvms-core-token-intent";
204
205const TOKEN_INTENT_NONE: &str = "none";
207
208fn request_token(request: &HttpRequest) -> Option<Option<&str>> {
209 match request.header(TOKEN_INTENT) {
210 None => None,
211 Some(TOKEN_INTENT_NONE) => Some(None),
212 Some(explicit) => Some(Some(explicit)),
213 }
214}
215
216fn unauthenticated(mut request: HttpRequest) -> HttpRequest {
218 request
219 .headers
220 .push((TOKEN_INTENT.to_string(), TOKEN_INTENT_NONE.to_string()));
221 request
222}
223
224#[derive(Clone)]
229pub struct Session {
230 transport: Arc<Transport>,
231 upload_identity: Arc<tokio::sync::OnceCell<(u32, u32)>>,
232 endpoint: String,
233 port: u16,
234 egress_posture: crate::control::EgressPosture,
235 clock: Arc<dyn Clock>,
238}
239
240impl Session {
241 pub fn agent_token(&self) -> &str {
249 &self.transport.agent_token
250 }
251
252 pub fn builder(endpoint: impl Into<String>, agent_token: impl Into<String>) -> SessionBuilder {
258 SessionBuilder {
259 endpoint: endpoint.into(),
260 agent_token: agent_token.into(),
261 minter: None,
262 backend: None,
263 port: DEFAULT_AGENT_PORT,
264 timeout: DEFAULT_REQUEST_TIMEOUT,
265 proxy: None,
266 egress_posture: crate::control::EgressPosture::default(),
267 clock: None,
268 }
269 }
270
271 pub fn endpoint(&self) -> &str {
273 &self.endpoint
274 }
275
276 pub(crate) fn mint_exec_id(&self) -> String {
278 exec_id_at(self.clock.unix_now())
279 }
280
281 pub fn egress_posture(&self) -> crate::control::EgressPosture {
289 self.egress_posture
290 }
291
292 pub fn port(&self) -> u16 {
294 self.port
295 }
296
297 pub fn proxy_auth(&self) -> Option<&Arc<ProxyAuth>> {
299 self.transport.proxy()
300 }
301
302 pub async fn connect_headers(&self, port: u16) -> Result<Vec<(String, String)>, Error> {
324 match self.transport.proxy() {
325 Some(proxy) => {
326 tokio::time::timeout(self.transport.timeout, proxy.headers_for_port(port))
327 .await
328 .map_err(|_| {
329 Error::wire(
330 WireKind::Transport,
331 "proxy authentication request timed out",
332 )
333 })?
334 }
335 None => Ok(Vec::new()),
336 }
337 }
338
339 pub async fn connect_subprotocols(&self, port: u16) -> Result<Option<[String; 3]>, Error> {
351 match self.transport.proxy() {
352 Some(proxy) => Ok(Some(
353 tokio::time::timeout(self.transport.timeout, proxy.subprotocols(port))
354 .await
355 .map_err(|_| {
356 Error::wire(
357 WireKind::Transport,
358 "proxy authentication request timed out",
359 )
360 })??,
361 )),
362 None => Ok(None),
363 }
364 }
365
366 pub fn rebind(&mut self, endpoint: String) {
378 self.endpoint = endpoint;
379 if let Some(proxy) = self.transport.proxy() {
380 proxy.invalidate();
381 }
382 }
383
384 pub async fn health(&self) -> Result<protocol::health::Health, Error> {
393 self.transport
394 .send_json(unauthenticated(HttpRequest::new("GET", "/v1/health")))
395 .await
396 }
397
398 pub async fn keep_awake(
403 &self,
404 policy: &KeepAwake,
405 stop: impl std::future::Future<Output = ()>,
406 ) -> Result<KeepAwakeReport, Error> {
407 policy
408 .run(|| async { self.health().await.map(Some) }, stop)
409 .await
410 }
411
412 pub async fn wait_until_ready(
420 &self,
421 timeout: Duration,
422 ) -> Result<protocol::health::Health, Error> {
423 let deadline = deadline_after(timeout);
424 let mut last: Option<Error> = None;
425 loop {
426 match self.health().await {
427 Ok(health) if health.bootstrapped => return Ok(health),
428 Ok(_) => {}
429 Err(err) if err.retryable() => last = Some(err),
430 Err(err) => return Err(err),
431 }
432 let now = tokio::time::Instant::now();
433 if now >= deadline {
434 let detail = match last {
435 Some(err) => format!(" (last error: {err})"),
436 None => String::new(),
437 };
438 return Err(Error::new(
439 ErrorKind::Timeout,
440 format!(
441 "the daemon was not bootstrapped within {}s{detail}",
442 timeout.as_secs()
443 ),
444 ));
445 }
446 tokio::time::sleep(READY_POLL_INTERVAL.min(deadline - now)).await;
447 }
448 }
449
450 pub async fn run(&self, req: protocol::exec::StartRequest) -> Result<ExecHandle, Error> {
462 if req.poweroff_on_exit {
463 let response = self
464 .transport
465 .send(unauthenticated(HttpRequest::new("GET", "/v1/health")))
466 .await?;
467 if response
468 .headers
469 .get(protocol::health::POWEROFF_ON_EXIT_HEADER)
470 .map(String::as_str)
471 != Some("1")
472 {
473 return Err(Error::invalid_arg(
474 "poweroff_on_exit requires a supporting daemon with CAP_SYS_BOOT",
475 ));
476 }
477 }
478 let requested = req.exec_id.clone();
479 let mut request = HttpRequest::new("POST", "/v1/exec/start");
480 request
481 .headers
482 .push(("content-type".into(), "application/json".into()));
483 request.body = serde_json::to_vec(&req).map_err(|err| {
484 Error::invalid_arg(format!("the start request will not serialize: {err}"))
485 })?;
486 let started: protocol::exec::StartResponse = self.transport.send_json(request).await?;
487 let exec_id = if started.exec_id.is_empty() {
492 requested
493 } else {
494 started.exec_id
495 };
496 Ok(ExecHandle::new(Arc::clone(&self.transport), exec_id))
497 }
498
499 pub fn exec(&self, exec_id: impl Into<String>) -> ExecHandle {
501 ExecHandle::new(Arc::clone(&self.transport), exec_id.into())
502 }
503
504 pub async fn run_sync(
506 &self,
507 req: protocol::exec::StartRequest,
508 timeout: Duration,
509 ) -> Result<ExecResult, Error> {
510 self.run(req).await?.wait_and_ack(timeout).await
511 }
512
513 pub async fn kill(&self, exec_id: &str) -> Result<bool, Error> {
515 self.exec(exec_id).kill().await
516 }
517
518 pub async fn procs(&self) -> Result<protocol::exec::ProcsResponse, Error> {
526 self.transport
527 .send_json(HttpRequest::new("GET", "/v1/procs"))
528 .await
529 }
530
531 pub async fn upload_file(
535 &self,
536 path: &str,
537 data: &[u8],
538 mode: Option<&str>,
539 ) -> Result<(), Error> {
540 upload::upload(self, path, data, mode, false).await
541 }
542
543 pub async fn download_file(&self, path: &str) -> Result<Vec<u8>, Error> {
545 files::download_file(&self.transport, path).await
546 }
547
548 pub async fn download_file_lines(
553 &self,
554 path: &str,
555 start_line: Option<u64>,
556 end_line: Option<u64>,
557 ) -> Result<Vec<u8>, Error> {
558 files::download_file_lines(&self.transport, path, start_line, end_line).await
559 }
560
561 pub async fn file_exists(&self, path: &str) -> Result<bool, Error> {
563 files::file_exists(&self.transport, path).await
564 }
565
566 pub async fn upload_tar(&self, remote: &str, archive: &[u8]) -> Result<(), Error> {
568 upload::upload(self, remote, archive, None, true).await
569 }
570
571 pub async fn download_tar(&self, remote: &str) -> Result<Vec<u8>, Error> {
573 files::download_tar(&self.transport, remote).await
574 }
575}
576
577impl std::fmt::Debug for Session {
578 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579 f.debug_struct("Session")
581 .field("endpoint", &self.endpoint)
582 .field("port", &self.port)
583 .field("proxy", &self.transport.proxy())
584 .finish()
585 }
586}
587
588pub struct SessionBuilder {
590 endpoint: String,
591 agent_token: String,
592 minter: Option<Arc<dyn TokenMinter>>,
593 backend: Option<http::SharedBackend>,
594 port: u16,
595 timeout: Duration,
596 proxy: Option<Arc<ProxyAuth>>,
597 egress_posture: crate::control::EgressPosture,
598 clock: Option<Arc<dyn Clock>>,
599}
600
601impl SessionBuilder {
602 #[must_use]
604 pub fn with_egress_posture(mut self, posture: crate::control::EgressPosture) -> Self {
605 self.egress_posture = posture;
606 self
607 }
608
609 #[must_use]
611 pub fn with_minter(mut self, minter: Arc<dyn TokenMinter>) -> Self {
612 self.minter = Some(minter);
613 self
614 }
615
616 #[must_use]
619 pub fn with_proxy_auth(mut self, proxy: Arc<ProxyAuth>) -> Self {
620 self.proxy = Some(proxy);
621 self
622 }
623
624 #[must_use]
626 pub fn with_backend(mut self, backend: http::SharedBackend) -> Self {
627 self.backend = Some(backend);
628 self
629 }
630
631 #[must_use]
633 pub fn with_port(mut self, port: u16) -> Self {
634 self.port = port;
635 self
636 }
637
638 #[must_use]
640 pub fn with_timeout(mut self, timeout: Duration) -> Self {
641 self.timeout = timeout;
642 self
643 }
644
645 #[must_use]
651 pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Self {
652 self.clock = Some(clock);
653 self
654 }
655
656 pub fn build_with(
663 mut self,
664 adapters: &dyn crate::adapters::Adapters,
665 ) -> Result<Session, Error> {
666 if self.backend.is_none() {
667 self.backend = Some(adapters.http_backend(&self.endpoint, self.timeout)?);
668 }
669 if self.clock.is_none() {
670 self.clock = Some(adapters.clock());
671 }
672 self.try_build()
673 }
674
675 pub fn try_build(self) -> Result<Session, Error> {
680 let (Some(backend), Some(clock)) = (self.backend, self.clock) else {
681 return Err(Error::new(
682 ErrorKind::Precondition,
683 "a session needs a backend and a clock: set both with with_backend and \
684 with_clock, or build through the prelude's SessionBuilder::build",
685 ));
686 };
687 let proxy = match (self.proxy, self.minter) {
688 (Some(proxy), _) => Some(proxy),
689 (None, Some(minter)) => Some(Arc::new(ProxyAuth::with_clock(
690 minter,
691 self.port,
692 Arc::clone(&clock),
693 ))),
694 (None, None) => None,
695 };
696 Ok(Session {
697 upload_identity: Arc::new(tokio::sync::OnceCell::new()),
698 transport: Arc::new(Transport {
699 backend,
700 agent_token: self.agent_token,
701 proxy,
702 timeout: self.timeout,
703 }),
704 endpoint: self.endpoint,
705 port: self.port,
706 egress_posture: self.egress_posture,
707 clock,
708 })
709 }
710}
711
712const FAR_FUTURE: Duration = Duration::from_secs(946_080_000);
716
717pub(crate) fn deadline_after(after: Duration) -> tokio::time::Instant {
725 let now = tokio::time::Instant::now();
726 now.checked_add(after).unwrap_or_else(|| now + FAR_FUTURE)
727}
728
729#[cfg(any(test, feature = "test-support"))]
730#[cfg_attr(test, mutants::skip)] pub(crate) mod testing {
732 use std::collections::HashMap;
740 use std::sync::{Mutex, PoisonError};
741
742 use futures_util::future::BoxFuture;
743
744 use super::http::{ChunkSource, HttpBackend, HttpRequest, HttpResponse, OpenStream};
745 use super::*;
746
747 pub enum Reply {
749 Body(u16, Vec<u8>),
751 Chunks(u16, Vec<Vec<u8>>),
753 Cut(&'static str),
755 Stalled(Vec<Vec<u8>>),
758 }
759
760 impl Reply {
761 pub fn json(status: u16, body: impl serde::Serialize) -> Self {
762 Reply::Body(status, serde_json::to_vec(&body).expect("test body"))
763 }
764
765 pub fn ok(body: impl serde::Serialize) -> Self {
766 Reply::json(200, body)
767 }
768 }
769
770 #[derive(Default)]
771 pub struct Recorder {
772 seen: Mutex<Vec<HttpRequest>>,
773 replies: Mutex<std::collections::VecDeque<Reply>>,
774 headers: HashMap<String, String>,
776 }
777
778 impl Recorder {
779 pub fn with(replies: impl IntoIterator<Item = Reply>) -> Arc<Self> {
780 Self::with_headers(replies, [])
781 }
782
783 pub fn with_headers(
785 replies: impl IntoIterator<Item = Reply>,
786 headers: impl IntoIterator<Item = (String, String)>,
787 ) -> Arc<Self> {
788 Arc::new(Self {
789 seen: Mutex::new(Vec::new()),
790 replies: Mutex::new(replies.into_iter().collect()),
791 headers: headers.into_iter().collect(),
792 })
793 }
794
795 pub fn requests(&self) -> Vec<HttpRequest> {
797 self.seen
798 .lock()
799 .unwrap_or_else(PoisonError::into_inner)
800 .clone()
801 }
802
803 pub fn last(&self) -> HttpRequest {
804 self.requests()
805 .pop()
806 .expect("the recorder saw no request at all")
807 }
808
809 fn record(&self, request: HttpRequest) -> Reply {
810 self.seen
811 .lock()
812 .unwrap_or_else(PoisonError::into_inner)
813 .push(request.clone());
814 self.replies
815 .lock()
816 .unwrap_or_else(PoisonError::into_inner)
817 .pop_front()
818 .unwrap_or_else(|| {
819 panic!(
820 "the recorder ran out of replies at {} {}",
821 request.method, request.path
822 )
823 })
824 }
825 }
826
827 struct Queued(std::collections::VecDeque<Vec<u8>>);
828
829 impl ChunkSource for Queued {
830 fn next_chunk(&mut self) -> BoxFuture<'_, Result<Option<Vec<u8>>, Error>> {
831 Box::pin(async move { Ok(self.0.pop_front()) })
832 }
833 }
834
835 struct Stalling(std::collections::VecDeque<Vec<u8>>);
837
838 impl ChunkSource for Stalling {
839 fn next_chunk(&mut self) -> BoxFuture<'_, Result<Option<Vec<u8>>, Error>> {
840 Box::pin(async move {
841 match self.0.pop_front() {
842 Some(chunk) => Ok(Some(chunk)),
843 None => std::future::pending().await,
844 }
845 })
846 }
847 }
848
849 impl HttpBackend for Recorder {
850 fn send(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, Error>> {
851 let reply = self.record(request);
852 Box::pin(async move {
853 match reply {
854 Reply::Body(status, body) => Ok(HttpResponse {
855 status,
856 headers: self.headers.clone(),
857 body,
858 }),
859 Reply::Chunks(status, chunks) => Ok(HttpResponse {
860 status,
861 headers: self.headers.clone(),
862 body: chunks.concat(),
863 }),
864 Reply::Stalled(chunks) => Ok(HttpResponse {
865 status: 200,
866 headers: self.headers.clone(),
867 body: chunks.concat(),
868 }),
869 Reply::Cut(why) => Err(Error::wire(WireKind::Transport, why)),
870 }
871 })
872 }
873
874 fn open_stream(
875 &self,
876 request: HttpRequest,
877 _idle_timeout: Duration,
878 ) -> BoxFuture<'_, Result<OpenStream, Error>> {
879 let reply = self.record(request);
880 Box::pin(async move {
881 let (status, chunks) = match reply {
882 Reply::Chunks(status, chunks) => (status, chunks),
883 Reply::Body(status, body) => (status, vec![body]),
884 Reply::Cut(why) => return Err(Error::wire(WireKind::Transport, why)),
885 Reply::Stalled(chunks) => {
886 let head = HttpResponse {
887 status: 200,
888 headers: self.headers.clone(),
889 body: Vec::new(),
890 };
891 let source: Box<dyn ChunkSource> =
892 Box::new(Stalling(chunks.into_iter().collect()));
893 return Ok((head, source));
894 }
895 };
896 let head = HttpResponse {
897 status,
898 headers: self.headers.clone(),
899 body: if (200..300).contains(&status) {
902 Vec::new()
903 } else {
904 chunks.concat()
905 },
906 };
907 let source: Box<dyn ChunkSource> = if (200..300).contains(&status) {
908 Box::new(Queued(chunks.into_iter().collect()))
909 } else {
910 Box::new(Queued(std::collections::VecDeque::new()))
911 };
912 Ok((head, source))
913 })
914 }
915 }
916
917 pub fn session_with(
919 recorder: Arc<Recorder>,
920 ) -> (
921 Session,
922 Arc<ProxyAuth>,
923 Arc<crate::clock::testing::TestClock>,
924 ) {
925 let clock = Arc::new(crate::clock::testing::TestClock::default());
926 let auth = Arc::new(
927 ProxyAuth::with_refresh_after(
928 Arc::new(proxy::testing::CountingMinter::default()),
929 DEFAULT_AGENT_PORT,
930 DEFAULT_REFRESH_AFTER,
931 Arc::clone(&clock) as Arc<dyn Clock>,
932 )
933 .expect("the default interval is below the ceiling"),
934 );
935 let session = Session::builder("https://vm.example", "agent-token-abcdef")
936 .with_backend(recorder)
937 .with_proxy_auth(Arc::clone(&auth))
938 .with_clock(Arc::clone(&clock) as Arc<dyn Clock>)
939 .try_build()
940 .expect("builds");
941 session
944 .upload_identity
945 .set((0, 0))
946 .expect("empty identity cache");
947 (session, auth, clock)
948 }
949
950 pub fn health_body(bootstrapped: bool) -> serde_json::Value {
951 serde_json::json!({
952 "version": "0.1.0",
953 "bootstrapped": bootstrapped,
954 "disk": null,
955 "identity_degraded": false,
956 "identity_repaired": true,
957 })
958 }
959
960 #[derive(Default)]
968 pub struct HealthyDaemon {
969 polls: std::sync::atomic::AtomicUsize,
970 }
971
972 impl HealthyDaemon {
973 pub fn new() -> Arc<Self> {
974 Arc::new(Self::default())
975 }
976
977 pub fn polls(&self) -> usize {
979 self.polls.load(std::sync::atomic::Ordering::SeqCst)
980 }
981
982 fn refuse(request: &HttpRequest) -> Error {
983 Error::wire(
984 WireKind::Transport,
985 format!(
986 "{} {} reached a test daemon that answers only health",
987 request.method, request.path
988 ),
989 )
990 }
991 }
992
993 impl HttpBackend for HealthyDaemon {
994 fn send(&self, request: HttpRequest) -> BoxFuture<'_, Result<HttpResponse, Error>> {
995 let answer = if request.method == "GET" && request.path == "/v1/health" {
996 self.polls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
997 Ok(HttpResponse {
998 status: 200,
999 headers: HashMap::new(),
1000 body: health_body(true).to_string().into_bytes(),
1001 })
1002 } else {
1003 Err(Self::refuse(&request))
1004 };
1005 Box::pin(async move { answer })
1006 }
1007
1008 fn open_stream(
1009 &self,
1010 request: HttpRequest,
1011 _idle_timeout: Duration,
1012 ) -> BoxFuture<'_, Result<OpenStream, Error>> {
1013 let refused = Self::refuse(&request);
1014 Box::pin(async move { Err(refused) })
1015 }
1016 }
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::testing::{Recorder, Reply, health_body, session_with};
1022 use super::*;
1023 use crate::session::proxy::testing::CountingMinter;
1024
1025 fn start_request(exec_id: &str) -> protocol::exec::StartRequest {
1026 protocol::exec::StartRequest::new(exec_id, vec!["/bin/true".to_string()])
1027 }
1028
1029 fn header(request: &HttpRequest, name: &str) -> Option<String> {
1030 request.header(name).map(str::to_string)
1031 }
1032
1033 fn header_of(headers: &[(String, String)], name: &str) -> Option<String> {
1035 headers
1036 .iter()
1037 .find(|(key, _)| key.eq_ignore_ascii_case(name))
1038 .map(|(_, value)| value.clone())
1039 }
1040
1041 #[test]
1048 fn try_build_needs_a_backend_and_a_clock() {
1049 let clock = Arc::new(crate::clock::testing::TestClock::new()) as Arc<dyn Clock>;
1050 let no_backend = Session::builder("https://vm.example", "token")
1051 .with_clock(Arc::clone(&clock))
1052 .try_build()
1053 .expect_err("no backend");
1054 assert_eq!(no_backend.kind(), ErrorKind::Precondition);
1055 let no_clock = Session::builder("https://vm.example", "token")
1056 .with_backend(Recorder::with([]))
1057 .try_build()
1058 .expect_err("no clock");
1059 assert_eq!(no_clock.kind(), ErrorKind::Precondition);
1060 Session::builder("https://vm.example", "token")
1061 .with_backend(Recorder::with([]))
1062 .with_clock(clock)
1063 .try_build()
1064 .expect("both set");
1065 }
1066
1067 #[tokio::test]
1073 async fn build_with_keeps_the_callers_backend() {
1074 let recorder = Recorder::with([Reply::ok(health_body(true))]);
1075 let session = Session::builder("http://127.0.0.1:9000", "token")
1076 .with_backend(Arc::clone(&recorder) as http::SharedBackend)
1077 .build_with(&crate::testing::TestAdapters::new())
1078 .expect("builds");
1079 session.health().await.expect("the recorder answered");
1080 assert_eq!(recorder.requests().len(), 1);
1081 }
1082
1083 #[tokio::test(start_paused = true)]
1084 async fn request_timeout_also_bounds_a_blocked_proxy_mint() {
1085 struct BlockedMinter;
1086 impl TokenMinter for BlockedMinter {
1087 fn mint(&self) -> futures_util::future::BoxFuture<'_, Result<ProxyToken, Error>> {
1088 Box::pin(std::future::pending())
1089 }
1090 }
1091 let backend = Recorder::with([]);
1092 let session = Session::builder("http://127.0.0.1:9", "private")
1093 .with_backend(backend.clone())
1094 .with_minter(Arc::new(BlockedMinter))
1095 .with_timeout(Duration::from_secs(3))
1096 .build_with(&crate::testing::TestAdapters::new())
1097 .expect("session");
1098 let result = tokio::time::timeout(Duration::from_secs(4), session.health())
1099 .await
1100 .expect("the request budget must include a stalled proxy mint")
1101 .expect_err("mint never completed");
1102 assert_eq!(result.wire_kind(), Some(WireKind::Transport));
1103 assert!(result.retryable());
1104 let result = tokio::time::timeout(Duration::from_secs(4), session.connect_headers(9000))
1105 .await
1106 .expect("external transfers must bound authentication too")
1107 .expect_err("mint remains blocked");
1108 assert!(result.retryable());
1109 let result =
1110 tokio::time::timeout(Duration::from_secs(4), session.connect_subprotocols(9000))
1111 .await
1112 .expect("external sockets must bound authentication too")
1113 .expect_err("mint remains blocked");
1114 assert!(result.retryable());
1115 assert!(
1116 backend.requests().is_empty(),
1117 "no request could authenticate"
1118 );
1119 }
1120
1121 #[tokio::test]
1129 async fn a_callers_content_type_survives_the_auth_header_injection() {
1130 let recorder = Recorder::with([Reply::ok(
1131 serde_json::json!({"exec_id":"e1","phase":"running"}),
1132 )]);
1133 let (session, _, _) = session_with(Arc::clone(&recorder));
1134
1135 session.run(start_request("e1")).await.expect("start");
1136
1137 let seen = recorder.requests();
1138 assert_eq!(
1139 header(&seen[0], "content-type").as_deref(),
1140 Some("application/json"),
1141 "the exec start lost its content type to the auth header injection"
1142 );
1143 }
1144
1145 #[tokio::test]
1150 async fn the_session_hands_back_the_token_it_authenticates_with() {
1151 let recorder = Recorder::with([]);
1152 let (session, _, _) = session_with(Arc::clone(&recorder));
1153 assert_eq!(session.agent_token(), "agent-token-abcdef");
1154 }
1155
1156 #[tokio::test]
1163 async fn every_endpoint_request_carries_both_proxy_headers() {
1164 let recorder = Recorder::with([
1165 Reply::ok(health_body(true)),
1166 Reply::ok(serde_json::json!({"exec_id":"e1","phase":"running"})),
1167 ]);
1168 let (session, _, _) = session_with(Arc::clone(&recorder));
1169
1170 session.health().await.expect("health succeeds");
1171 session.run(start_request("e1")).await.expect("start");
1172
1173 let seen = recorder.requests();
1174 assert_eq!(seen.len(), 2);
1175 for request in &seen {
1176 assert_eq!(
1177 header(request, PROXY_AUTH_HEADER).as_deref(),
1178 Some(CountingMinter::value(0).as_str()),
1179 "{} {} went out without the proxy auth header",
1180 request.method,
1181 request.path
1182 );
1183 assert_eq!(
1184 header(request, PROXY_PORT_HEADER).as_deref(),
1185 Some("9000"),
1186 "{} {} went out without the proxy port header, which the proxy \
1187 rejects as if the token were bad",
1188 request.method,
1189 request.path
1190 );
1191 }
1192 }
1193
1194 #[tokio::test]
1200 async fn health_sends_no_bearer_and_an_exec_route_does() {
1201 let recorder = Recorder::with([
1202 Reply::ok(health_body(true)),
1203 Reply::ok(serde_json::json!({"exec_id":"e1","phase":"running"})),
1204 ]);
1205 let (session, _, _) = session_with(Arc::clone(&recorder));
1206
1207 session.health().await.expect("health");
1208 let seen = recorder.requests();
1209 assert_eq!(header(&seen[0], "authorization"), None);
1210 assert_eq!(
1211 header(&seen[0], "x-microvms-core-token-intent"),
1212 None,
1213 "the internal token-intent marker must not reach the wire"
1214 );
1215
1216 session.run(start_request("e1")).await.expect("start");
1217 assert_eq!(
1218 header(&recorder.last(), "authorization").as_deref(),
1219 Some("Bearer agent-token-abcdef")
1220 );
1221 }
1222
1223 #[tokio::test(start_paused = true)]
1226 async fn keep_awake_polls_health_through_the_proxy_until_the_vm_is_idle() {
1227 let busy = |busy: bool| {
1228 let mut body = health_body(true);
1229 body["busy"] = serde_json::json!(busy);
1230 Reply::ok(body)
1231 };
1232 let recorder = Recorder::with([busy(true), busy(true), busy(false)]);
1233 let (session, _, _) = session_with(Arc::clone(&recorder));
1234 let report = session
1235 .clone()
1236 .keep_awake(
1237 &KeepAwake::new(None).while_busy(true),
1238 std::future::pending(),
1239 )
1240 .await
1241 .expect("keepalive");
1242 assert_eq!((report.end, report.polls), (KeepAwakeEnd::Idle, 3));
1243 let seen = recorder.requests();
1244 assert_eq!(seen.len(), 3);
1245 for request in &seen {
1246 assert_eq!(
1247 (request.method, request.path.as_str()),
1248 ("GET", "/v1/health")
1249 );
1250 assert_eq!(header(request, "authorization"), None);
1251 assert!(header(request, PROXY_AUTH_HEADER).is_some());
1252 assert!(header(request, PROXY_PORT_HEADER).is_some());
1253 }
1254 }
1255
1256 #[tokio::test]
1259 async fn a_direct_session_sends_no_proxy_headers() {
1260 let recorder = Recorder::with([Reply::ok(health_body(true))]);
1261 let session = Session::builder("http://127.0.0.1:9000", "token")
1262 .with_backend(Arc::clone(&recorder) as http::SharedBackend)
1263 .build_with(&crate::testing::TestAdapters::new())
1264 .expect("builds");
1265 assert!(session.proxy_auth().is_none());
1266
1267 session.health().await.expect("health");
1268 let request = recorder.last();
1269 assert_eq!(header(&request, PROXY_AUTH_HEADER), None);
1270 assert_eq!(header(&request, PROXY_PORT_HEADER), None);
1271 }
1272
1273 #[tokio::test]
1276 async fn a_mint_failure_surfaces_as_a_retryable_request_error() {
1277 let recorder = Recorder::with([Reply::ok(health_body(true))]);
1278 let auth = Arc::new(
1279 ProxyAuth::with_refresh_after(
1280 Arc::new(CountingMinter::failing(1)),
1281 DEFAULT_AGENT_PORT,
1282 DEFAULT_REFRESH_AFTER,
1283 Arc::new(proxy::testing::TestClock::default()),
1284 )
1285 .expect("interval accepted"),
1286 );
1287 let session = Session::builder("https://vm.example", "token")
1288 .with_backend(Arc::clone(&recorder) as http::SharedBackend)
1289 .with_proxy_auth(Arc::clone(&auth))
1290 .build_with(&crate::testing::TestAdapters::new())
1291 .expect("builds");
1292
1293 let err = session.health().await.expect_err("the mint fails");
1294 assert_eq!(err.kind(), ErrorKind::Retryable);
1295 assert_eq!(err.wire_kind(), Some(WireKind::AuthTokenMint));
1296 assert!(
1297 recorder.requests().is_empty(),
1298 "a request went out without a token"
1299 );
1300
1301 session.health().await.expect("the retry succeeds");
1302 assert_eq!(auth.mint_count(), 1);
1303 }
1304
1305 #[tokio::test]
1308 async fn a_daemon_status_arrives_as_its_typed_error() {
1309 for (status, expected) in [
1310 (401, WireKind::Unauthorized),
1311 (404, WireKind::NotFound),
1312 (409, WireKind::Conflict),
1313 (503, WireKind::NotBootstrapped),
1314 ] {
1315 let recorder = Recorder::with([Reply::Body(status, b"{\"error\":\"x\"}".to_vec())]);
1316 let (session, _, _) = session_with(recorder);
1317 let err = session.health().await.expect_err("a failing status");
1318 assert_eq!(err.wire_kind(), Some(expected), "status {status}");
1319 }
1320 }
1321
1322 #[tokio::test(start_paused = true)]
1325 async fn wait_until_ready_polls_through_not_bootstrapped_and_a_dropped_connection() {
1326 let recorder = Recorder::with([
1327 Reply::Cut("connection refused"),
1328 Reply::Body(503, b"not bootstrapped".to_vec()),
1329 Reply::ok(health_body(false)),
1330 Reply::ok(health_body(true)),
1331 ]);
1332 let (session, _, _) = session_with(Arc::clone(&recorder));
1333
1334 let health = session
1335 .wait_until_ready(DEFAULT_BOOTSTRAP_TIMEOUT)
1336 .await
1337 .expect("the daemon comes up");
1338 assert!(health.bootstrapped);
1339 assert_eq!(recorder.requests().len(), 4);
1340 }
1341
1342 #[tokio::test(start_paused = true)]
1345 async fn wait_until_ready_gives_up_at_once_on_a_credential_failure() {
1346 let recorder = Recorder::with([Reply::Body(401, b"wrong token".to_vec())]);
1347 let (session, _, _) = session_with(Arc::clone(&recorder));
1348
1349 let err = session
1350 .wait_until_ready(DEFAULT_BOOTSTRAP_TIMEOUT)
1351 .await
1352 .expect_err("a 401 is fatal");
1353 assert_eq!(err.kind(), ErrorKind::Credentials);
1354 assert_eq!(
1355 recorder.requests().len(),
1356 1,
1357 "a fatal error was retried, so a wrong token costs the full timeout"
1358 );
1359 }
1360
1361 #[tokio::test(start_paused = true)]
1364 async fn wait_until_ready_times_out_naming_the_last_retryable_error() {
1365 let recorder = Recorder::with([
1366 Reply::Cut("connection refused"),
1367 Reply::Cut("connection refused"),
1368 Reply::Cut("connection refused"),
1369 ]);
1370 let (session, _, _) = session_with(recorder);
1371
1372 let err = session
1373 .wait_until_ready(Duration::from_secs(4))
1374 .await
1375 .expect_err("the daemon never comes up");
1376 assert_eq!(err.kind(), ErrorKind::Timeout);
1377 assert!(
1378 err.to_string().contains("connection refused"),
1379 "the timeout must name what it kept hitting: {err}"
1380 );
1381 }
1382
1383 #[tokio::test]
1389 async fn rebind_invalidates_the_proxy_token_so_the_next_request_mints() {
1390 let recorder = Recorder::with([Reply::ok(health_body(true)), Reply::ok(health_body(true))]);
1391 let (mut session, auth, _) = session_with(Arc::clone(&recorder));
1392
1393 session.health().await.expect("health");
1394 assert_eq!(auth.mint_count(), 1);
1395
1396 session.rebind("https://vm.example".to_string());
1397 assert!(!auth.is_cached(), "rebind did not drop the token");
1398
1399 session.health().await.expect("health");
1400 assert_eq!(auth.mint_count(), 2);
1401 assert_eq!(
1402 header(&recorder.last(), PROXY_AUTH_HEADER).as_deref(),
1403 Some(CountingMinter::value(1).as_str()),
1404 "the request after a rebind carried the pre-suspend token"
1405 );
1406 }
1407
1408 #[tokio::test]
1411 async fn a_start_request_serializes_through_the_protocol_crate() {
1412 let recorder = Recorder::with([Reply::ok(
1413 serde_json::json!({"exec_id":"e-confirmed","phase":"running"}),
1414 )]);
1415 let (session, _, _) = session_with(Arc::clone(&recorder));
1416
1417 let handle = session.run(start_request("e-asked")).await.expect("start");
1418 assert_eq!(
1419 handle.exec_id(),
1420 "e-confirmed",
1421 "the handle addresses the id the client asked for rather than the one the \
1422 daemon confirmed, so every later call would 404 if they diverged"
1423 );
1424
1425 let request = recorder.last();
1426 assert_eq!(request.method, "POST");
1427 assert_eq!(request.path, "/v1/exec/start");
1428 let body: serde_json::Value = serde_json::from_slice(&request.body).expect("json body");
1429 assert_eq!(body["exec_id"], "e-asked");
1430 assert_eq!(body["command"], serde_json::json!(["/bin/true"]));
1431 }
1432
1433 #[tokio::test]
1436 async fn an_unreadable_response_body_names_the_schema_route() {
1437 let recorder = Recorder::with([Reply::Body(200, b"{\"unexpected\":true}".to_vec())]);
1438 let (session, _, _) = session_with(recorder);
1439 let err = session.health().await.expect_err("the body will not parse");
1440 assert_eq!(err.wire_kind(), Some(WireKind::ProtocolError));
1441 assert!(err.to_string().contains("/v1/schema"), "{err}");
1442 }
1443
1444 #[tokio::test]
1465 async fn a_session_answers_connect_credentials_for_a_port_off_its_own_token_cache() {
1466 let recorder = Recorder::with([Reply::ok(health_body(true))]);
1467 let (session, auth, _) = session_with(Arc::clone(&recorder));
1468
1469 session.health().await.expect("health warms the cache");
1470 assert_eq!(auth.mint_count(), 1);
1471
1472 let headers = session
1473 .connect_headers(8080)
1474 .await
1475 .expect("the cached token answers");
1476 let offered = session
1477 .connect_subprotocols(8080)
1478 .await
1479 .expect("the cached token answers")
1480 .expect("a proxied session offers subprotocols");
1481
1482 assert_eq!(
1483 header_of(&headers, PROXY_PORT_HEADER).as_deref(),
1484 Some("8080"),
1485 "the caller asked for 8080 and would have targeted the session's port"
1486 );
1487 assert_eq!(
1489 header_of(&headers, PROXY_AUTH_HEADER).as_deref(),
1490 Some(CountingMinter::value(1).as_str()),
1491 "the header pair carried the agent-port-only token, which the proxy refuses for \
1492 8080 with 403 'Access to port denied'"
1493 );
1494 assert_eq!(offered[0], proxy::WS_SUBPROTOCOL);
1495 assert_eq!(
1496 offered[1],
1497 format!(
1498 "{}{}",
1499 proxy::WS_AUTH_SUBPROTOCOL_PREFIX,
1500 CountingMinter::value(1)
1501 ),
1502 "the handshake carried a different token than the header path, so they are two \
1503 caches rather than one"
1504 );
1505 assert_eq!(offered[2], "lambda-microvms.port.8080");
1506 assert_eq!(
1507 auth.mint_count(),
1508 2,
1509 "one mint covered both the agent port and 8080, which the control plane does not \
1510 do — or the second accessor minted a third, which would be two caches"
1511 );
1512 assert!(
1513 recorder.requests().len() == 1,
1514 "building credentials sent a request to the VM: {:?}",
1515 recorder.requests()
1516 );
1517 }
1518
1519 #[tokio::test]
1530 async fn a_direct_session_offers_no_subprotocols_and_empty_headers() {
1531 let recorder = Recorder::with([]);
1532 let session = Session::builder("http://127.0.0.1:9000", "token")
1533 .with_backend(recorder as http::SharedBackend)
1534 .build_with(&crate::testing::TestAdapters::new())
1535 .expect("builds");
1536
1537 assert!(
1538 session
1539 .connect_headers(9000)
1540 .await
1541 .expect("no proxy is not a failure")
1542 .is_empty(),
1543 "a direct session's requests carry no proxy headers, so neither should this"
1544 );
1545 assert!(
1546 session
1547 .connect_subprotocols(9000)
1548 .await
1549 .expect("no proxy is not a failure")
1550 .is_none(),
1551 "a handshake with the base subprotocol and no token is refused for a reason that \
1552 names neither"
1553 );
1554 }
1555
1556 #[tokio::test]
1567 async fn connect_credentials_re_mint_when_the_refresh_window_has_rolled_over() {
1568 let recorder = Recorder::with([]);
1569 let (session, auth, clock) = session_with(recorder);
1570
1571 let first = session
1572 .connect_subprotocols(9000)
1573 .await
1574 .expect("mints")
1575 .expect("proxied");
1576 assert!(first[1].ends_with(&CountingMinter::value(0)));
1577
1578 clock.advance(DEFAULT_REFRESH_AFTER + Duration::from_secs(1));
1579 let second = session
1580 .connect_subprotocols(9000)
1581 .await
1582 .expect("re-mints")
1583 .expect("proxied");
1584
1585 assert_eq!(auth.mint_count(), 2);
1586 assert!(
1587 second[1].ends_with(&CountingMinter::value(1)),
1588 "the handshake would carry a token past its refresh window: {}",
1589 second[1]
1590 );
1591 }
1592
1593 #[tokio::test]
1603 async fn procs_is_a_bearer_get_of_the_procs_route_with_a_typed_body() {
1604 let recorder = Recorder::with([Reply::ok(serde_json::json!({
1605 "procs": [{
1606 "exec_id": "e1",
1607 "pgid": 4242,
1608 "started_at": 1_756_500_000u64,
1609 "child_exited": true,
1610 "reap": false,
1611 "pids": [4243],
1612 }]
1613 }))]);
1614 let (session, _, _) = session_with(Arc::clone(&recorder));
1615
1616 let listed = session.procs().await.expect("procs");
1617 let request = recorder.last();
1618 assert_eq!(request.method, "GET");
1619 assert_eq!(request.path, "/v1/procs");
1620 assert_eq!(
1621 header(&request, "authorization").as_deref(),
1622 Some("Bearer agent-token-abcdef"),
1623 "the procs route is bearer-guarded; without the token it is a 503 or a 401"
1624 );
1625 assert_eq!(listed.procs.len(), 1);
1626 assert_eq!(listed.procs[0].exec_id, "e1");
1627 assert_eq!(listed.procs[0].pgid, Some(4242));
1628 assert!(listed.procs[0].child_exited);
1629 assert_eq!(listed.procs[0].pids, [4243]);
1630 }
1631
1632 #[tokio::test]
1640 async fn a_start_request_carries_the_reap_flag_on_the_wire() {
1641 let recorder = Recorder::with([Reply::ok(
1642 serde_json::json!({"exec_id":"e1","phase":"running"}),
1643 )]);
1644 let (session, _, _) = session_with(Arc::clone(&recorder));
1645
1646 let mut request = start_request("e1");
1647 request.reap_group_on_exit = true;
1648 session.run(request).await.expect("start");
1649
1650 let body: serde_json::Value =
1651 serde_json::from_slice(&recorder.last().body).expect("a JSON start body");
1652 assert_eq!(
1653 body["reap_group_on_exit"],
1654 serde_json::Value::Bool(true),
1655 "the daemon defaults a missing flag to false, so a dropped key is a silent \
1656 behaviour change: {body}"
1657 );
1658 }
1659
1660 #[tokio::test]
1661 async fn poweroff_requires_explicit_peer_support_before_starting() {
1662 for value in [None, Some("0"), Some("true"), Some("2")] {
1663 let headers = value
1664 .map(|value| {
1665 (
1666 protocol::health::POWEROFF_ON_EXIT_HEADER.into(),
1667 value.into(),
1668 )
1669 })
1670 .into_iter();
1671 let recorder = Recorder::with_headers([Reply::Body(200, b"{}".to_vec())], headers);
1672 let (session, _, _) = session_with(Arc::clone(&recorder));
1673 let error = session
1674 .run(start_request("poweroff").with_poweroff_on_exit(true))
1675 .await
1676 .expect_err("a peer must explicitly support poweroff");
1677 assert!(error.to_string().contains("poweroff_on_exit"));
1678 let requests = recorder.requests();
1679 assert_eq!(requests.len(), 1, "an unsupported peer must not start work");
1680 assert_eq!(requests[0].method, "GET");
1681 assert_eq!(requests[0].path, "/v1/health");
1682 assert!(header(&requests[0], "authorization").is_none());
1683 }
1684 }
1685
1686 #[tokio::test]
1687 async fn poweroff_opt_in_is_gated_and_the_default_needs_no_extra_request() {
1688 for enabled in [false, true] {
1689 let mut replies = Vec::new();
1690 if enabled {
1691 replies.push(Reply::Body(200, b"{}".to_vec()));
1692 }
1693 replies.push(Reply::ok(
1694 serde_json::json!({"exec_id":"e1","phase":"running"}),
1695 ));
1696 let headers = enabled.then_some((
1697 protocol::health::POWEROFF_ON_EXIT_HEADER.to_owned(),
1698 "1".to_owned(),
1699 ));
1700 let recorder = Recorder::with_headers(replies, headers);
1701 let (session, _, _) = session_with(Arc::clone(&recorder));
1702 session
1703 .run(start_request("e1").with_poweroff_on_exit(enabled))
1704 .await
1705 .unwrap();
1706 assert_eq!(recorder.requests().len(), if enabled { 2 } else { 1 });
1707 let request = recorder.last();
1708 assert_eq!(request.method, "POST");
1709 assert_eq!(request.path, "/v1/exec/start");
1710 let body: serde_json::Value = serde_json::from_slice(&request.body).unwrap();
1711 assert_eq!(
1712 body.get("poweroff_on_exit"),
1713 enabled.then_some(&serde_json::Value::Bool(true))
1714 );
1715 }
1716 }
1717
1718 #[test]
1720 fn a_session_debug_does_not_print_the_agent_token() {
1721 let recorder = Recorder::with([]);
1722 let session = Session::builder("https://vm.example", "super-secret-token")
1723 .with_backend(recorder as http::SharedBackend)
1724 .build_with(&crate::testing::TestAdapters::new())
1725 .expect("builds");
1726 let rendered = format!("{session:?}");
1727 assert!(rendered.contains("vm.example"), "{rendered}");
1728 assert!(
1729 !rendered.contains("super-secret"),
1730 "the agent token reached a Debug string: {rendered}"
1731 );
1732 }
1733
1734 #[tokio::test(start_paused = true)]
1749 async fn a_ready_wait_longer_than_the_clock_can_hold_still_polls() {
1750 let recorder =
1751 Recorder::with([Reply::ok(health_body(false)), Reply::ok(health_body(true))]);
1752 let (session, _, _) = session_with(Arc::clone(&recorder));
1753
1754 let health = session
1755 .wait_until_ready(Duration::MAX)
1756 .await
1757 .expect("a far deadline waits for the bootstrap");
1758 assert!(health.bootstrapped);
1759 assert_eq!(
1760 recorder.requests().len(),
1761 2,
1762 "the wait polled past the first not-bootstrapped answer"
1763 );
1764 }
1765}
1766
1767#[cfg(test)]
1768mod sse_fuzz;