1use std::net::SocketAddr;
74use std::sync::Arc;
75
76use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
77use tokio::net::{TcpListener, TcpStream};
78
79use microvms_app::error::{Error, ErrorKind};
80use microvms_app::session::proxy::{PROXY_AUTH_HEADER, PROXY_PORT_HEADER, ProxyAuth};
81
82pub const DEFAULT_EXCHANGE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
89
90#[derive(Clone, Debug)]
97pub enum ForwardEvent {
98 Listening { local: SocketAddr, guest_port: u16 },
101 Forwarded { status: u16, upgraded: bool },
103 Refused { status: u16, explanation: String },
109 ConnectionError { detail: String },
111}
112
113pub fn refusal_explanation(status: u16, guest_port: u16) -> Option<String> {
118 match status {
119 403 => Some(format!(
120 "the endpoint proxy refused the request for port {guest_port} (403 Access to port \
121 denied): the minted token's scope does not cover it. Nothing was wrong with the \
122 guest — the credential never authorized this port."
123 )),
124 502 => Some(format!(
125 "the endpoint proxy reached the VM and found nothing listening on port \
126 {guest_port} (502). The credential is correct and the scope covers the port, so \
127 this is a dead server in the guest rather than an auth problem."
128 )),
129 401 => Some(
130 "the endpoint proxy rejected the credential (401). On a long-lived tunnel this \
131 usually means a minted token expired without being refreshed; the mint count is \
132 the observable that says whether a refresh happened."
133 .to_string(),
134 ),
135 _ => None,
136 }
137}
138
139pub async fn splice<A, B>(a: &mut A, b: &mut B) -> Result<(u64, u64), Error>
145where
146 A: AsyncRead + AsyncWrite + Unpin + ?Sized,
147 B: AsyncRead + AsyncWrite + Unpin + ?Sized,
148{
149 tokio::io::copy_bidirectional(a, b).await.map_err(|err| {
150 Error::new(
151 ErrorKind::Unexpected,
152 format!("the upgraded connection ended abnormally: {err}"),
153 )
154 })
155}
156
157#[derive(Clone, Debug)]
163pub struct ForwardSpec {
164 pub bind: SocketAddr,
169 pub guest_port: u16,
171 pub endpoint: String,
173 pub exchange_timeout: std::time::Duration,
175}
176
177impl ForwardSpec {
178 pub fn new(bind: SocketAddr, guest_port: u16, endpoint: impl Into<String>) -> Self {
180 Self {
181 bind,
182 guest_port,
183 endpoint: endpoint.into(),
184 exchange_timeout: DEFAULT_EXCHANGE_TIMEOUT,
185 }
186 }
187}
188
189pub fn upstream_url(endpoint: &str, target: &str) -> String {
195 let base = endpoint.trim_end_matches('/');
196 let base = if base.starts_with("https://") || base.starts_with("http://") {
197 base.to_string()
198 } else {
199 format!("https://{base}")
200 };
201 if target.starts_with('/') {
202 format!("{base}{target}")
203 } else {
204 format!("{base}/{target}")
205 }
206}
207
208pub fn direct_url(endpoint: &str, guest_port: u16, target: &str) -> Result<String, Error> {
213 let mut url = reqwest::Url::parse(&upstream_url(endpoint, target)).map_err(|err| {
214 Error::new(
215 ErrorKind::InvalidArg,
216 format!("{endpoint} is not an endpoint a request can be addressed to: {err}"),
217 )
218 })?;
219 url.set_port(Some(guest_port)).map_err(|()| {
220 Error::new(
221 ErrorKind::InvalidArg,
222 format!("{endpoint} names no host a port can be set on"),
223 )
224 })?;
225 Ok(url.to_string())
226}
227
228fn local_host_matches(headers: &[(String, String)], bind: SocketAddr) -> bool {
231 let mut hosts = headers
232 .iter()
233 .filter(|(name, _)| name.eq_ignore_ascii_case("host"));
234 let Some((_, value)) = hosts.next() else {
235 return false;
236 };
237 if hosts.next().is_some() {
238 return false;
239 }
240 let Ok(authority) = value.parse::<http::uri::Authority>() else {
241 return false;
242 };
243 if value.contains('@') {
244 return false;
245 }
246 let port = match authority.port_u16() {
247 Some(port) => port,
248 None if authority.as_str() == authority.host() => 80,
249 None => return false,
250 };
251 if port != bind.port() {
252 return false;
253 }
254 let host = authority.host();
255 if host.eq_ignore_ascii_case("localhost") {
256 return bind.ip().is_loopback();
257 }
258 let host = host
259 .strip_prefix('[')
260 .and_then(|host| host.strip_suffix(']'))
261 .unwrap_or(host);
262 host.parse::<std::net::IpAddr>().is_ok_and(|ip| {
263 if bind.ip().is_loopback() {
264 ip.is_loopback()
265 } else {
266 ip == bind.ip()
267 }
268 })
269}
270
271pub const STRIPPED_REQUEST_HEADERS: [&str; 3] = [PROXY_AUTH_HEADER, PROXY_PORT_HEADER, "host"];
279
280pub fn forwards_request_header(name: &str) -> bool {
282 !STRIPPED_REQUEST_HEADERS
283 .iter()
284 .any(|stripped| stripped.eq_ignore_ascii_case(name))
285}
286
287pub struct ForwardClient {
294 inner: reqwest::Client,
295}
296
297impl ForwardClient {
298 pub fn new() -> Result<Self, Error> {
300 reqwest::Client::builder()
301 .redirect(reqwest::redirect::Policy::none())
303 .build()
304 .map(|inner| Self { inner })
305 .map_err(|err| {
306 Error::new(
307 ErrorKind::Unexpected,
308 format!("could not build the forwarder's HTTP client: {err}"),
309 )
310 })
311 }
312}
313
314impl std::fmt::Debug for ForwardClient {
315 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316 f.debug_struct("ForwardClient").finish_non_exhaustive()
317 }
318}
319
320pub async fn bind(spec: &ForwardSpec) -> Result<TcpListener, Error> {
326 TcpListener::bind(spec.bind).await.map_err(|err| {
327 Error::new(
328 ErrorKind::InvalidArg,
329 format!(
330 "could not bind {}: {err}. Another process is probably already listening \
331 there; pass a different local port.",
332 spec.bind
333 ),
334 )
335 })
336}
337
338pub async fn hop_headers(
343 auth: &Arc<ProxyAuth>,
344 guest_port: u16,
345) -> Result<Vec<(String, String)>, Error> {
346 auth.headers_for_port(guest_port).await
347}
348
349pub async fn accept(listener: &TcpListener) -> Result<(TcpStream, SocketAddr), Error> {
354 listener.accept().await.map_err(|err| {
355 Error::new(
356 ErrorKind::Unexpected,
357 format!("the local listener stopped accepting: {err}"),
358 )
359 })
360}
361
362pub fn is_upgrade(status: u16) -> bool {
369 status == 101
370}
371
372pub trait LocalStream: AsyncRead + AsyncWrite + Unpin + Send {}
379impl<T: AsyncRead + AsyncWrite + Unpin + Send> LocalStream for T {}
380
381pub async fn serve_connection<S, F>(
397 local: S,
398 spec: &ForwardSpec,
399 auth: &Arc<ProxyAuth>,
400 client: &ForwardClient,
401 on_event: F,
402) -> Result<(), Error>
403where
404 S: LocalStream,
405 F: FnMut(ForwardEvent),
406{
407 serve_connection_via(local, spec, Some(auth), client, on_event).await
408}
409
410pub(crate) async fn serve_connection_via<S, F>(
417 mut local: S,
418 spec: &ForwardSpec,
419 auth: Option<&Arc<ProxyAuth>>,
420 client: &ForwardClient,
421 mut on_event: F,
422) -> Result<(), Error>
423where
424 S: LocalStream,
425 F: FnMut(ForwardEvent),
426{
427 let head = match read_request_head(&mut local).await? {
428 Some(head) => head,
429 None => return Ok(()),
432 };
433
434 if !local_host_matches(&head.headers, spec.bind) {
435 let explanation = "the request Host must name the local forward and its port".to_string();
436 write_local_error(&mut local, 403, &explanation).await?;
437 on_event(ForwardEvent::Refused {
438 status: 403,
439 explanation,
440 });
441 return Ok(());
442 }
443
444 let (minted, url) = match auth {
447 Some(auth) => (
448 hop_headers(auth, spec.guest_port).await?,
449 upstream_url(&spec.endpoint, &head.target),
450 ),
451 None => (
452 Vec::new(),
453 direct_url(&spec.endpoint, spec.guest_port, &head.target)?,
454 ),
455 };
456 let method = reqwest::Method::from_bytes(head.method.as_bytes()).map_err(|err| {
457 Error::new(
458 ErrorKind::InvalidArg,
459 format!(
460 "the local client sent an unusable method {:?}: {err}",
461 head.method
462 ),
463 )
464 })?;
465
466 let mut request = client
467 .inner
468 .request(method, &url)
469 .timeout(spec.exchange_timeout);
470 for (name, value) in head
471 .headers
472 .iter()
473 .filter(|(name, _)| forwards_request_header(name))
474 {
475 request = request.header(name, value);
476 }
477 for (name, value) in &minted {
478 request = request.header(name, value);
479 }
480 if !head.body.is_empty() {
481 request = request.body(head.body.clone());
482 }
483
484 let response = request.send().await.map_err(|err| {
485 Error::new(
486 ErrorKind::Unexpected,
487 format!(
488 "forwarding {} {} to the endpoint failed: {err}",
489 head.method, head.target
490 ),
491 )
492 })?;
493 let status = response.status().as_u16();
494
495 if let Some(explanation) = refusal_explanation(status, spec.guest_port) {
496 on_event(ForwardEvent::Refused {
497 status,
498 explanation: explanation.clone(),
499 });
500 write_local_error(&mut local, status, &explanation).await?;
501 return Ok(());
502 }
503
504 if is_upgrade(status) {
505 write_response_head(&mut local, status, response.headers()).await?;
509 let mut upstream = response.upgrade().await.map_err(|err| {
510 Error::new(
511 ErrorKind::Unexpected,
512 format!("the endpoint answered 101 but the connection did not upgrade: {err}"),
513 )
514 })?;
515 on_event(ForwardEvent::Forwarded {
516 status,
517 upgraded: true,
518 });
519 splice(&mut local, &mut upstream).await?;
520 return Ok(());
521 }
522
523 let headers = response.headers().clone();
524 let body = response.bytes().await.map_err(|err| {
525 Error::new(
526 ErrorKind::Unexpected,
527 format!("reading the guest's response body failed: {err}"),
528 )
529 })?;
530 write_response(&mut local, status, &headers, &body).await?;
531 on_event(ForwardEvent::Forwarded {
532 status,
533 upgraded: false,
534 });
535 Ok(())
536}
537
538#[derive(Clone, Debug)]
545struct RequestHead {
546 method: String,
547 target: String,
548 headers: Vec<(String, String)>,
549 body: Vec<u8>,
550}
551
552async fn read_request_head<S>(local: &mut S) -> Result<Option<RequestHead>, Error>
557where
558 S: AsyncRead + Unpin + ?Sized,
559{
560 use tokio::io::AsyncReadExt as _;
561
562 let mut buffer = Vec::new();
563 let mut byte = [0_u8; 1];
564 loop {
568 match local.read(&mut byte).await {
569 Ok(0) if buffer.is_empty() => return Ok(None),
570 Ok(0) => {
571 return Err(Error::new(
572 ErrorKind::Protocol,
573 "the local client closed the connection mid-request".to_string(),
574 ));
575 }
576 Ok(_) => {
577 buffer.push(byte[0]);
578 if buffer.len() > MAX_REQUEST_HEAD_BYTES {
579 return Err(Error::new(
580 ErrorKind::InvalidArg,
581 format!(
582 "the local client's request head exceeded {MAX_REQUEST_HEAD_BYTES} \
583 bytes without terminating"
584 ),
585 ));
586 }
587 if buffer.ends_with(b"\r\n\r\n") {
588 break;
589 }
590 }
591 Err(err) => {
592 return Err(Error::new(
593 ErrorKind::Unexpected,
594 format!("reading the local client's request failed: {err}"),
595 ));
596 }
597 }
598 }
599
600 let text = String::from_utf8(buffer).map_err(|err| {
601 Error::new(
602 ErrorKind::Protocol,
603 format!("the local client's request head is not utf-8: {err}"),
604 )
605 })?;
606 let mut lines = text.split("\r\n");
607 let request_line = lines.next().unwrap_or_default();
608 let mut parts = request_line.split_whitespace();
609 let method = parts.next().unwrap_or_default().to_string();
610 let target = parts.next().unwrap_or_default().to_string();
611 if method.is_empty() || target.is_empty() {
612 return Err(Error::new(
613 ErrorKind::Protocol,
614 format!("the local client sent an unparseable request line {request_line:?}"),
615 ));
616 }
617
618 let mut headers = Vec::new();
619 for line in lines {
620 if line.is_empty() {
621 continue;
622 }
623 let Some((name, value)) = line.split_once(':') else {
624 return Err(Error::new(
625 ErrorKind::Protocol,
626 format!("the local client sent an unparseable header line {line:?}"),
627 ));
628 };
629 headers.push((name.trim().to_string(), value.trim().to_string()));
630 }
631
632 if headers.iter().any(|(name, value)| {
633 name.eq_ignore_ascii_case("transfer-encoding") && value.contains("chunked")
634 }) {
635 return Err(Error::new(
636 ErrorKind::InvalidArg,
637 "the local client sent a chunked request body, which this forwarder does not \
638 re-frame. Send a content-length body, or report the client that needs chunked \
639 uploads so the forwarder can grow a streaming path."
640 .to_string(),
641 ));
642 }
643
644 let length: usize = headers
645 .iter()
646 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
647 .and_then(|(_, value)| value.parse().ok())
648 .unwrap_or(0);
649
650 let mut body = vec![0_u8; length];
651 if length > 0 {
652 local.read_exact(&mut body).await.map_err(|err| {
653 Error::new(
654 ErrorKind::Protocol,
655 format!("the local client announced {length} body bytes and sent fewer: {err}"),
656 )
657 })?;
658 }
659
660 Ok(Some(RequestHead {
661 method,
662 target,
663 headers,
664 body,
665 }))
666}
667
668const MAX_REQUEST_HEAD_BYTES: usize = 64 * 1024;
671
672const RECOMPUTED_RESPONSE_HEADERS: [&str; 2] = ["content-length", "transfer-encoding"];
678
679async fn write_response_head<W>(
681 local: &mut W,
682 status: u16,
683 headers: &reqwest::header::HeaderMap,
684) -> Result<(), Error>
685where
686 W: AsyncWrite + Unpin + ?Sized,
687{
688 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
689 for (name, value) in headers {
690 if let Ok(value) = value.to_str() {
691 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
692 }
693 }
694 head.push_str("\r\n");
695 local.write_all(head.as_bytes()).await.map_err(|err| {
696 Error::new(
697 ErrorKind::Unexpected,
698 format!("could not write the response head to the local client: {err}"),
699 )
700 })?;
701 local.flush().await.map_err(|err| {
702 Error::new(
703 ErrorKind::Unexpected,
704 format!("could not flush the response head: {err}"),
705 )
706 })
707}
708
709async fn write_response<W>(
711 local: &mut W,
712 status: u16,
713 headers: &reqwest::header::HeaderMap,
714 body: &[u8],
715) -> Result<(), Error>
716where
717 W: AsyncWrite + Unpin + ?Sized,
718{
719 let mut head = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
720 for (name, value) in headers {
721 if RECOMPUTED_RESPONSE_HEADERS
722 .iter()
723 .any(|skip| skip.eq_ignore_ascii_case(name.as_str()))
724 {
725 continue;
726 }
727 if let Ok(value) = value.to_str() {
728 head.push_str(&format!("{}: {}\r\n", name.as_str(), value));
729 }
730 }
731 head.push_str(&format!("content-length: {}\r\n\r\n", body.len()));
732
733 local.write_all(head.as_bytes()).await.map_err(|err| {
734 Error::new(
735 ErrorKind::Unexpected,
736 format!("could not write the response to the local client: {err}"),
737 )
738 })?;
739 local.write_all(body).await.map_err(|err| {
740 Error::new(
741 ErrorKind::Unexpected,
742 format!("could not write the response body to the local client: {err}"),
743 )
744 })?;
745 local.flush().await.map_err(|err| {
746 Error::new(
747 ErrorKind::Unexpected,
748 format!("could not flush the response: {err}"),
749 )
750 })
751}
752
753fn reason_phrase(status: u16) -> &'static str {
758 match status {
759 101 => "Switching Protocols",
760 200 => "OK",
761 403 => "Forbidden",
762 404 => "Not Found",
763 500 => "Internal Server Error",
764 502 => "Bad Gateway",
765 504 => "Gateway Timeout",
766 _ => "Status",
767 }
768}
769
770pub async fn write_local_error<W>(stream: &mut W, status: u16, body: &str) -> Result<(), Error>
776where
777 W: AsyncWrite + Unpin + ?Sized,
778{
779 let reason = match status {
780 403 => "Forbidden",
781 502 => "Bad Gateway",
782 504 => "Gateway Timeout",
783 _ => "Bad Gateway",
784 };
785 let response = format!(
786 "HTTP/1.1 {status} {reason}\r\ncontent-type: text/plain; charset=utf-8\r\n\
787 content-length: {}\r\nconnection: close\r\n\r\n{body}",
788 body.len()
789 );
790 stream.write_all(response.as_bytes()).await.map_err(|err| {
791 Error::new(
792 ErrorKind::Unexpected,
793 format!("could not answer the local client: {err}"),
794 )
795 })?;
796 stream.flush().await.map_err(|err| {
797 Error::new(
798 ErrorKind::Unexpected,
799 format!("could not flush the local client's response: {err}"),
800 )
801 })
802}
803
804#[cfg(test)]
805mod tests {
806 use super::*;
807 use microvms_app::session::proxy::DEFAULT_AGENT_PORT;
808 use microvms_app::testing::{CountingMinter, TestClock};
809
810 fn auth_pair() -> (Arc<ProxyAuth>, Arc<CountingMinter>) {
813 let minter = Arc::new(CountingMinter::default());
814 let auth = ProxyAuth::with_refresh_after(
815 minter.clone(),
816 DEFAULT_AGENT_PORT,
817 std::time::Duration::from_secs(30 * 60),
818 Arc::new(TestClock::default()),
819 )
820 .expect("the default refresh interval is below the ceiling");
821 (Arc::new(auth), minter)
822 }
823
824 async fn upstream(response: &'static str) -> (String, tokio::task::JoinHandle<String>) {
831 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
832 let addr = listener.local_addr().expect("bound");
833 let handle = tokio::spawn(async move {
834 let (mut socket, _) =
837 tokio::time::timeout(std::time::Duration::from_secs(10), listener.accept())
838 .await
839 .expect("the forwarder reaches the upstream")
840 .expect("one connection");
841 let head = read_request_head(&mut socket)
842 .await
843 .expect("a readable request")
844 .expect("a request arrived");
845 socket
846 .write_all(response.as_bytes())
847 .await
848 .expect("the canned response is written");
849 socket.flush().await.expect("flushed");
850 let headers = head
851 .headers
852 .iter()
853 .map(|(name, value)| format!("{}: {value}", name.to_ascii_lowercase()))
854 .collect::<Vec<_>>()
855 .join("\n");
856 format!("{} {}\n{headers}", head.method, head.target)
857 });
858 (format!("http://{addr}"), handle)
859 }
860
861 async fn exchange(
864 request: &str,
865 endpoint: String,
866 guest_port: u16,
867 auth: &Arc<ProxyAuth>,
868 ) -> (String, Vec<ForwardEvent>) {
869 let (mut client, server) = tokio::io::duplex(64 * 1024);
870 let spec = ForwardSpec::new(
871 "127.0.0.1:8080".parse().expect("an addr"),
872 guest_port,
873 endpoint,
874 );
875 let http = ForwardClient::new().expect("a client");
876
877 let auth = auth.clone();
878 let served = tokio::spawn(async move {
879 let mut events = Vec::new();
880 let result =
881 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
882 (result, events)
883 });
884
885 client
886 .write_all(request.as_bytes())
887 .await
888 .expect("the request is written");
889 client.flush().await.expect("flushed");
890
891 let (result, events) = served.await.expect("the task joins");
892 result.expect("the exchange completes");
893
894 let mut seen = Vec::new();
895 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut seen)
896 .await
897 .expect("the client reads the response");
898 (String::from_utf8_lossy(&seen).to_string(), events)
899 }
900
901 #[tokio::test]
902 async fn requests_name_the_local_listener_before_minting() {
903 for host in [
904 "",
905 "example.com:8080",
906 "localhost:8081",
907 "127.0.0.1",
908 "localhost:8080@elsewhere.example",
909 "user@localhost:8080",
910 "localhost:8080/path",
911 "localhost:8080\r\nHost: localhost:8080",
912 ] {
913 let (auth, _) = auth_pair();
914 let request = if host.is_empty() {
915 "GET / HTTP/1.1\r\n\r\n".to_string()
916 } else {
917 format!("GET / HTTP/1.1\r\nHost: {host}\r\n\r\n")
918 };
919 let (response, events) =
920 exchange(&request, "http://127.0.0.1:1".to_string(), 8080, &auth).await;
921 assert!(
922 response.starts_with("HTTP/1.1 403 Forbidden"),
923 "{host}: {response}"
924 );
925 assert_eq!(auth.mint_count(), 0, "{host}");
926 assert!(matches!(
927 events.as_slice(),
928 [ForwardEvent::Refused { status: 403, .. }]
929 ));
930 }
931 }
932
933 #[test]
934 fn host_authorities_match_the_bound_address_and_port() {
935 let bind = "127.0.0.1:8080".parse().expect("an address");
936 for host in [
937 "localhost:8080",
938 "LOCALHOST:8080",
939 "127.0.0.1:8080",
940 "[::1]:8080",
941 ] {
942 assert!(
943 local_host_matches(&[("Host".into(), host.into())], bind),
944 "{host}"
945 );
946 }
947 assert!(local_host_matches(
948 &[("Host".into(), "localhost".into())],
949 "127.0.0.1:80".parse().unwrap()
950 ));
951 for host in ["localhost", "localhost:8081", "localhost:0"] {
952 assert!(!local_host_matches(&[("Host".into(), host.into())], bind));
953 }
954 assert!(!local_host_matches(
955 &[("Host".into(), "localhost:invalid".into())],
956 bind
957 ));
958 assert!(!local_host_matches(
959 &[("Host".into(), "localhost:65536".into())],
960 bind
961 ));
962 assert!(!local_host_matches(
963 &[("Host".into(), "localhost:65536".into())],
964 "127.0.0.1:80".parse().unwrap()
965 ));
966 assert!(!local_host_matches(
967 &[("Host".into(), "localhost:8080".into())],
968 "192.0.2.1:8080".parse().unwrap()
969 ));
970 assert!(local_host_matches(
971 &[("Host".into(), "192.0.2.1:8080".into())],
972 "192.0.2.1:8080".parse().unwrap()
973 ));
974 }
975
976 #[tokio::test]
977 async fn an_ordinary_get_reaches_the_guest_carrying_both_minted_headers() {
978 let (endpoint, upstream_saw) =
981 upstream("HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello").await;
982 let (auth, minter) = auth_pair();
983
984 let (seen, events) = exchange(
985 "GET /index.html HTTP/1.1\r\nhost: localhost:8080\r\nuser-agent: probe\r\n\r\n",
986 endpoint,
987 8080,
988 &auth,
989 )
990 .await;
991
992 let request = upstream_saw.await.expect("the upstream task joins");
993 assert!(request.starts_with("GET /index.html"), "{request}");
994 assert!(
995 request.contains(&format!(
996 "{}: jwe-0",
997 PROXY_AUTH_HEADER.to_ascii_lowercase()
998 )),
999 "the minted auth header did not reach upstream: {request}"
1000 );
1001 assert!(
1002 request.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
1003 "the port header did not name the guest port: {request}"
1004 );
1005 assert!(request.contains("user-agent: probe"), "{request}");
1006
1007 assert!(seen.starts_with("HTTP/1.1 200 OK\r\n"), "{seen}");
1008 assert!(seen.ends_with("hello"), "{seen}");
1009 assert!(
1010 matches!(
1011 events.as_slice(),
1012 [ForwardEvent::Forwarded {
1013 status: 200,
1014 upgraded: false
1015 }]
1016 ),
1017 "{events:?}"
1018 );
1019
1020 let requested = minter.requested();
1023 assert!(
1024 requested.iter().any(|ports| ports.contains(&8080)),
1025 "the mint never asked for port 8080: {requested:?}"
1026 );
1027 }
1028
1029 #[tokio::test]
1033 async fn the_caller_cannot_smuggle_its_own_proxy_headers_upstream() {
1034 let (endpoint, upstream_saw) =
1035 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1036 let (auth, _minter) = auth_pair();
1037
1038 let request = format!(
1039 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n{PROXY_AUTH_HEADER}: forged-jwe\r\n\
1040 {PROXY_PORT_HEADER}: 1\r\n\r\n"
1041 );
1042 let (_seen, _events) = exchange(&request, endpoint, 8080, &auth).await;
1043
1044 let saw = upstream_saw.await.expect("the upstream task joins");
1045 assert!(
1046 !saw.contains("forged-jwe"),
1047 "a caller's forged credential was forwarded: {saw}"
1048 );
1049 assert!(
1050 saw.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
1051 "the minted port header should be the only one: {saw}"
1052 );
1053 assert!(
1054 !saw.contains("host: localhost:8080"),
1055 "the local host header must not override the endpoint's: {saw}"
1056 );
1057 }
1058
1059 #[tokio::test]
1060 async fn a_scope_refusal_is_answered_with_the_sentence_that_names_it() {
1061 let (endpoint, upstream_saw) =
1064 upstream("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n").await;
1065 let (auth, _minter) = auth_pair();
1066
1067 let (seen, events) = exchange(
1068 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1069 endpoint,
1070 8080,
1071 &auth,
1072 )
1073 .await;
1074 let _ = upstream_saw.await;
1075
1076 assert!(seen.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{seen}");
1077 assert!(seen.contains("scope does not cover"), "{seen}");
1078 match events.as_slice() {
1079 [
1080 ForwardEvent::Refused {
1081 status: 403,
1082 explanation,
1083 },
1084 ] => {
1085 assert!(explanation.contains("8080"), "{explanation}");
1086 }
1087 other => panic!("expected one 403 refusal, got {other:?}"),
1088 }
1089 }
1090
1091 #[tokio::test]
1092 async fn a_dead_guest_server_is_reported_as_502_rather_than_as_an_auth_problem() {
1093 let (endpoint, upstream_saw) =
1094 upstream("HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\n\r\n").await;
1095 let (auth, _minter) = auth_pair();
1096
1097 let (seen, events) = exchange(
1098 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1099 endpoint,
1100 5432,
1101 &auth,
1102 )
1103 .await;
1104 let _ = upstream_saw.await;
1105
1106 assert!(seen.contains("nothing listening on port 5432"), "{seen}");
1107 assert!(
1108 matches!(
1109 events.as_slice(),
1110 [ForwardEvent::Refused { status: 502, .. }]
1111 ),
1112 "{events:?}"
1113 );
1114 }
1115
1116 #[tokio::test]
1117 async fn the_guests_own_404_is_relayed_and_not_reinterpreted() {
1118 let (endpoint, upstream_saw) =
1121 upstream("HTTP/1.1 404 Not Found\r\ncontent-length: 9\r\n\r\nno-route!").await;
1122 let (auth, _minter) = auth_pair();
1123
1124 let (seen, events) = exchange(
1125 "GET /nope HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1126 endpoint,
1127 8080,
1128 &auth,
1129 )
1130 .await;
1131 let _ = upstream_saw.await;
1132
1133 assert!(seen.starts_with("HTTP/1.1 404 Not Found\r\n"), "{seen}");
1134 assert!(seen.ends_with("no-route!"), "{seen}");
1135 assert!(
1136 matches!(
1137 events.as_slice(),
1138 [ForwardEvent::Forwarded { status: 404, .. }]
1139 ),
1140 "{events:?}"
1141 );
1142 }
1143
1144 #[tokio::test]
1145 async fn a_post_body_is_forwarded_and_the_response_length_is_re_derived() {
1146 let (endpoint, upstream_saw) = upstream(
1149 "HTTP/1.1 200 OK\r\ncontent-length: 3\r\ntransfer-encoding: identity\r\n\r\nack",
1150 )
1151 .await;
1152 let (auth, _minter) = auth_pair();
1153
1154 let (seen, _events) = exchange(
1155 "POST /submit HTTP/1.1\r\nhost: localhost:8080\r\ncontent-length: 7\r\n\r\npayload",
1156 endpoint,
1157 8080,
1158 &auth,
1159 )
1160 .await;
1161 let saw = upstream_saw.await.expect("the upstream task joins");
1162
1163 assert!(saw.starts_with("POST /submit"), "{saw}");
1164 assert!(saw.contains("content-length: 7"), "{saw}");
1165 assert_eq!(
1166 seen.matches("content-length:").count(),
1167 1,
1168 "exactly one framing header should be written: {seen}"
1169 );
1170 assert!(seen.contains("content-length: 3\r\n"), "{seen}");
1171 assert!(
1172 !seen.to_ascii_lowercase().contains("transfer-encoding"),
1173 "the upstream framing header must not be copied: {seen}"
1174 );
1175 assert!(seen.ends_with("ack"), "{seen}");
1176 }
1177
1178 #[tokio::test]
1179 async fn a_101_relays_the_handshake_and_then_stops_parsing() {
1180 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1184 let addr = listener.local_addr().expect("bound");
1185 let guest = tokio::spawn(async move {
1186 let (mut socket, _) = listener.accept().await.expect("one connection");
1187 let _ = read_request_head(&mut socket)
1188 .await
1189 .expect("a request")
1190 .expect("present");
1191 socket
1192 .write_all(
1193 b"HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\n\
1194 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1195 )
1196 .await
1197 .expect("the handshake is written");
1198 socket.flush().await.expect("flushed");
1199 let mut frame = [0_u8; 5];
1201 tokio::io::AsyncReadExt::read_exact(&mut socket, &mut frame)
1202 .await
1203 .expect("the client's bytes arrive");
1204 socket
1205 .write_all(b"PONG")
1206 .await
1207 .expect("the answer is written");
1208 socket.flush().await.expect("flushed");
1209 frame.to_vec()
1210 });
1211
1212 let (auth, _minter) = auth_pair();
1213 let (mut client, server) = tokio::io::duplex(64 * 1024);
1214 let spec = ForwardSpec::new(
1215 "127.0.0.1:8080".parse().expect("an addr"),
1216 8080,
1217 format!("http://{addr}"),
1218 );
1219 let http = ForwardClient::new().expect("a client");
1220 let served = tokio::spawn(async move {
1221 let mut events = Vec::new();
1222 let result =
1223 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
1224 (result, events)
1225 });
1226
1227 client
1228 .write_all(
1229 b"GET /socket HTTP/1.1\r\nhost: localhost:8080\r\nupgrade: websocket\r\n\
1230 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1231 )
1232 .await
1233 .expect("the handshake request is written");
1234 client.flush().await.expect("flushed");
1235
1236 let mut head = Vec::new();
1239 let mut byte = [0_u8; 1];
1240 while !head.ends_with(b"\r\n\r\n") {
1241 tokio::io::AsyncReadExt::read_exact(&mut client, &mut byte)
1242 .await
1243 .expect("the handshake response arrives");
1244 head.push(byte[0]);
1245 }
1246 let head = String::from_utf8_lossy(&head).to_string();
1247 assert!(
1248 head.starts_with("HTTP/1.1 101 Switching Protocols\r\n"),
1249 "{head}"
1250 );
1251 assert!(
1252 head.to_ascii_lowercase()
1253 .contains("sec-websocket-protocol: my-app-protocol"),
1254 "the guest's negotiated subprotocol must reach the client: {head}"
1255 );
1256
1257 client
1258 .write_all(b"PING!")
1259 .await
1260 .expect("a frame is written");
1261 client.flush().await.expect("flushed");
1262 let mut answer = [0_u8; 4];
1263 tokio::io::AsyncReadExt::read_exact(&mut client, &mut answer)
1264 .await
1265 .expect("the guest's answer arrives");
1266 assert_eq!(&answer, b"PONG");
1267
1268 drop(client);
1269 let guest_saw = guest.await.expect("the guest task joins");
1270 assert_eq!(
1271 &guest_saw, b"PING!",
1272 "the client's bytes reached the guest unframed"
1273 );
1274
1275 let (result, events) = served.await.expect("the task joins");
1276 result.expect("the splice ends cleanly");
1277 assert!(
1278 events.iter().any(|event| matches!(
1279 event,
1280 ForwardEvent::Forwarded {
1281 status: 101,
1282 upgraded: true
1283 }
1284 )),
1285 "{events:?}"
1286 );
1287 }
1288
1289 #[tokio::test]
1290 async fn every_exchange_mints_through_the_cache_rather_than_capturing_headers_once() {
1291 let (auth, minter) = auth_pair();
1295
1296 for _ in 0..2 {
1297 let (endpoint, upstream_saw) =
1298 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1299 let (_seen, _events) = exchange(
1300 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1301 endpoint,
1302 8080,
1303 &auth,
1304 )
1305 .await;
1306 let saw = upstream_saw.await.expect("the upstream task joins");
1307 assert!(
1308 saw.contains(&PROXY_AUTH_HEADER.to_ascii_lowercase()),
1309 "every hop carries the minted credential: {saw}"
1310 );
1311 }
1312
1313 assert_eq!(
1317 auth.mint_count(),
1318 1,
1319 "the second hop should reuse the cached token rather than re-mint"
1320 );
1321 assert_eq!(minter.requested().len(), 1);
1322 }
1323
1324 #[tokio::test]
1325 async fn a_chunked_request_body_is_refused_by_name_rather_than_truncated() {
1326 let (mut client, mut server) = tokio::io::duplex(4096);
1329 client
1330 .write_all(b"POST /up HTTP/1.1\r\nhost: local\r\ntransfer-encoding: chunked\r\n\r\n")
1331 .await
1332 .expect("written");
1333 client.flush().await.expect("flushed");
1334
1335 let error = read_request_head(&mut server)
1336 .await
1337 .expect_err("chunked is refused");
1338 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1339 assert!(error.to_string().contains("chunked"), "{error}");
1340 }
1341
1342 #[tokio::test]
1343 async fn a_connection_that_closes_before_sending_anything_is_not_an_error() {
1344 let (client, mut server) = tokio::io::duplex(4096);
1347 drop(client);
1348 let head = read_request_head(&mut server)
1349 .await
1350 .expect("an empty connection is fine");
1351 assert!(head.is_none());
1352 }
1353
1354 #[tokio::test]
1355 async fn an_unterminated_request_head_is_capped_rather_than_buffered_without_bound() {
1356 let (mut client, mut server) = tokio::io::duplex(1024 * 1024);
1357 let writer = tokio::spawn(async move {
1358 let filler = vec![b'x'; MAX_REQUEST_HEAD_BYTES + 1024];
1360 let _ = client.write_all(b"GET / HTTP/1.1\r\nx: ").await;
1361 let _ = client.write_all(&filler).await;
1362 let _ = client.flush().await;
1363 });
1364
1365 let error = read_request_head(&mut server)
1366 .await
1367 .expect_err("the cap fires");
1368 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1369 assert!(error.to_string().contains("without terminating"), "{error}");
1370 writer.abort();
1371 }
1372
1373 #[test]
1374 fn only_a_101_counts_as_an_upgrade() {
1375 assert!(is_upgrade(101));
1379 assert!(!is_upgrade(200));
1380 assert!(!is_upgrade(426));
1381 }
1382
1383 #[test]
1384 fn a_bare_endpoint_host_is_read_as_https() {
1385 assert_eq!(
1388 upstream_url("vm-abc.example.aws", "/index.html"),
1389 "https://vm-abc.example.aws/index.html"
1390 );
1391 }
1392
1393 #[test]
1394 fn an_explicit_scheme_is_left_alone() {
1395 assert_eq!(
1396 upstream_url("https://vm-abc.example.aws/", "/a"),
1397 "https://vm-abc.example.aws/a"
1398 );
1399 assert_eq!(
1400 upstream_url("http://127.0.0.1:9000", "/a"),
1401 "http://127.0.0.1:9000/a"
1402 );
1403 }
1404
1405 #[test]
1406 fn a_target_without_a_leading_slash_still_forms_one_path() {
1407 assert_eq!(
1408 upstream_url("vm.example", "index.html"),
1409 "https://vm.example/index.html"
1410 );
1411 }
1412
1413 #[test]
1414 fn the_two_proxy_headers_are_never_copied_from_the_caller() {
1415 assert!(!forwards_request_header("x-aws-proxy-auth"));
1418 assert!(!forwards_request_header("X-AWS-PROXY-AUTH"));
1419 assert!(!forwards_request_header("x-aws-proxy-port"));
1420 assert!(!forwards_request_header("Host"));
1421 assert!(forwards_request_header("user-agent"));
1422 assert!(forwards_request_header("sec-websocket-protocol"));
1423 }
1424
1425 #[test]
1426 fn the_refusal_wording_separates_a_scope_mistake_from_a_dead_server() {
1427 let scope = refusal_explanation(403, 8080).expect("403 explains itself");
1430 assert!(scope.contains("scope does not cover"), "{scope}");
1431 assert!(scope.contains("8080"), "{scope}");
1432
1433 let dead = refusal_explanation(502, 8080).expect("502 explains itself");
1434 assert!(dead.contains("nothing listening"), "{dead}");
1435 assert!(dead.contains("8080"), "{dead}");
1436
1437 assert!(
1438 refusal_explanation(401, 8080).is_some(),
1439 "401 is worth naming"
1440 );
1441 assert!(
1442 refusal_explanation(200, 8080).is_none(),
1443 "a success explains nothing"
1444 );
1445 assert!(
1446 refusal_explanation(404, 8080).is_none(),
1447 "the guest's own 404 is not ours"
1448 );
1449 }
1450
1451 #[tokio::test]
1452 async fn a_bind_collision_names_the_address_and_suggests_another_port() {
1453 let held = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1454 let addr = held.local_addr().expect("a bound address");
1455
1456 let spec = ForwardSpec::new(addr, 8080, "vm.example");
1457 let error = bind(&spec).await.expect_err("the address is taken");
1458 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1459 assert!(error.to_string().contains(&addr.to_string()), "{error}");
1460 assert!(
1461 error.to_string().contains("different local port"),
1462 "{error}"
1463 );
1464 }
1465
1466 #[tokio::test]
1467 async fn a_local_error_response_is_a_readable_http_message() {
1468 let (mut client, mut server) = tokio::io::duplex(4096);
1471 let body = refusal_explanation(403, 8080).expect("wording");
1472 write_local_error(&mut server, 403, &body)
1473 .await
1474 .expect("the response is written");
1475 drop(server);
1476
1477 let mut got = Vec::new();
1478 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut got)
1479 .await
1480 .expect("the client reads it");
1481 let text = String::from_utf8(got).expect("utf-8");
1482 assert!(text.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{text}");
1483 assert!(text.contains("content-length: "), "{text}");
1484 assert!(text.ends_with(&body), "{text}");
1485 }
1486
1487 #[tokio::test]
1488 async fn splice_copies_both_directions_and_reports_the_byte_counts() {
1489 let (mut left_near, mut left_far) = tokio::io::duplex(4096);
1490 let (mut right_near, mut right_far) = tokio::io::duplex(4096);
1491
1492 let pump = tokio::spawn(async move { splice(&mut left_far, &mut right_near).await });
1493
1494 left_near.write_all(b"to-guest").await.expect("write");
1495 left_near.shutdown().await.expect("half close");
1496 right_far.write_all(b"from-guest!").await.expect("write");
1497 right_far.shutdown().await.expect("half close");
1498
1499 let mut to_guest = Vec::new();
1500 tokio::io::AsyncReadExt::read_to_end(&mut right_far, &mut to_guest)
1501 .await
1502 .expect("read");
1503 let mut from_guest = Vec::new();
1504 tokio::io::AsyncReadExt::read_to_end(&mut left_near, &mut from_guest)
1505 .await
1506 .expect("read");
1507
1508 let (a_to_b, b_to_a) = pump.await.expect("the task joins").expect("a clean splice");
1509 assert_eq!(to_guest, b"to-guest");
1510 assert_eq!(from_guest, b"from-guest!");
1511 assert_eq!(a_to_b, 8);
1512 assert_eq!(b_to_a, 11);
1513 }
1514}