1use std::net::SocketAddr;
74use std::sync::atomic::{AtomicBool, Ordering};
75use std::sync::{Arc, Mutex};
76
77use futures_util::StreamExt as _;
78use http_body_util::combinators::UnsyncBoxBody;
79use http_body_util::{BodyExt as _, Full, Limited, StreamBody};
80use hyper::body::{Bytes, Frame, Incoming};
81use hyper::service::service_fn;
82use hyper_util::rt::{TokioIo, TokioTimer};
83
84use tokio::io::{AsyncRead, AsyncWrite};
85use tokio::net::{TcpListener, TcpStream};
86use tokio::sync::mpsc;
87
88use microvms_app::error::{Error, ErrorKind};
89use microvms_app::session::proxy::{PROXY_AUTH_HEADER, PROXY_PORT_HEADER, ProxyAuth};
90
91pub const DEFAULT_EXCHANGE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
100
101#[derive(Clone, Debug)]
108pub enum ForwardEvent {
109 Listening { local: SocketAddr, guest_port: u16 },
112 Forwarded { status: u16, upgraded: bool },
114 Refused { status: u16, explanation: String },
120 ConnectionError { detail: String },
122}
123
124pub fn refusal_explanation(status: u16, guest_port: u16) -> Option<String> {
129 match status {
130 403 => Some(format!(
131 "the endpoint proxy refused the request for port {guest_port} (403 Access to port \
132 denied): the minted token's scope does not cover it. Nothing was wrong with the \
133 guest — the credential never authorized this port."
134 )),
135 502 => Some(format!(
136 "the endpoint proxy reached the VM and found nothing listening on port \
137 {guest_port} (502). The credential is correct and the scope covers the port, so \
138 this is a dead server in the guest rather than an auth problem."
139 )),
140 401 => Some(
141 "the endpoint proxy rejected the credential (401). On a long-lived tunnel this \
142 usually means a minted token expired without being refreshed; the mint count is \
143 the observable that says whether a refresh happened."
144 .to_string(),
145 ),
146 _ => None,
147 }
148}
149
150pub async fn splice<A, B>(a: &mut A, b: &mut B) -> Result<(u64, u64), Error>
156where
157 A: AsyncRead + AsyncWrite + Unpin + ?Sized,
158 B: AsyncRead + AsyncWrite + Unpin + ?Sized,
159{
160 tokio::io::copy_bidirectional(a, b).await.map_err(|err| {
161 Error::new(
162 ErrorKind::Unexpected,
163 format!("the upgraded connection ended abnormally: {err}"),
164 )
165 })
166}
167
168#[derive(Clone, Debug)]
174pub struct ForwardSpec {
175 pub bind: SocketAddr,
180 pub guest_port: u16,
182 pub endpoint: String,
184 pub exchange_timeout: std::time::Duration,
188}
189
190impl ForwardSpec {
191 pub fn new(bind: SocketAddr, guest_port: u16, endpoint: impl Into<String>) -> Self {
193 Self {
194 bind,
195 guest_port,
196 endpoint: endpoint.into(),
197 exchange_timeout: DEFAULT_EXCHANGE_TIMEOUT,
198 }
199 }
200}
201
202pub fn upstream_url(endpoint: &str, target: &str) -> String {
208 let base = endpoint.trim_end_matches('/');
209 let base = if base.starts_with("https://") || base.starts_with("http://") {
210 base.to_string()
211 } else {
212 format!("https://{base}")
213 };
214 if target.starts_with('/') {
215 format!("{base}{target}")
216 } else {
217 format!("{base}/{target}")
218 }
219}
220
221pub fn direct_url(endpoint: &str, guest_port: u16, target: &str) -> Result<String, Error> {
226 let mut url = reqwest::Url::parse(&upstream_url(endpoint, target)).map_err(|err| {
227 Error::new(
228 ErrorKind::InvalidArg,
229 format!("{endpoint} is not an endpoint a request can be addressed to: {err}"),
230 )
231 })?;
232 url.set_port(Some(guest_port)).map_err(|()| {
233 Error::new(
234 ErrorKind::InvalidArg,
235 format!("{endpoint} names no host a port can be set on"),
236 )
237 })?;
238 Ok(url.to_string())
239}
240
241fn local_host_matches(headers: &[(String, String)], bind: SocketAddr) -> bool {
244 let mut hosts = headers
245 .iter()
246 .filter(|(name, _)| name.eq_ignore_ascii_case("host"));
247 let Some((_, value)) = hosts.next() else {
248 return false;
249 };
250 if hosts.next().is_some() {
251 return false;
252 }
253 let Ok(authority) = value.parse::<http::uri::Authority>() else {
254 return false;
255 };
256 if value.contains('@') {
257 return false;
258 }
259 let port = match authority.port_u16() {
260 Some(port) => port,
261 None if authority.as_str() == authority.host() => 80,
262 None => return false,
263 };
264 if port != bind.port() {
265 return false;
266 }
267 let host = authority.host();
268 if host.eq_ignore_ascii_case("localhost") {
269 return bind.ip().is_loopback();
270 }
271 let host = host
272 .strip_prefix('[')
273 .and_then(|host| host.strip_suffix(']'))
274 .unwrap_or(host);
275 host.parse::<std::net::IpAddr>().is_ok_and(|ip| {
276 if bind.ip().is_loopback() {
277 ip.is_loopback()
278 } else {
279 ip == bind.ip()
280 }
281 })
282}
283
284pub const STRIPPED_REQUEST_HEADERS: [&str; 3] = [PROXY_AUTH_HEADER, PROXY_PORT_HEADER, "host"];
292
293pub fn forwards_request_header(name: &str) -> bool {
295 !STRIPPED_REQUEST_HEADERS
296 .iter()
297 .any(|stripped| stripped.eq_ignore_ascii_case(name))
298}
299
300pub struct ForwardClient {
307 inner: reqwest::Client,
308}
309
310impl ForwardClient {
311 pub fn new() -> Result<Self, Error> {
313 reqwest::Client::builder()
314 .redirect(reqwest::redirect::Policy::none())
316 .no_gzip()
319 .no_brotli()
320 .no_zstd()
321 .no_deflate()
322 .build()
323 .map(|inner| Self { inner })
324 .map_err(|err| {
325 Error::new(
326 ErrorKind::Unexpected,
327 format!("could not build the forwarder's HTTP client: {err}"),
328 )
329 })
330 }
331}
332
333impl std::fmt::Debug for ForwardClient {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 f.debug_struct("ForwardClient").finish_non_exhaustive()
336 }
337}
338
339pub async fn bind(spec: &ForwardSpec) -> Result<TcpListener, Error> {
345 TcpListener::bind(spec.bind).await.map_err(|err| {
346 Error::new(
347 ErrorKind::InvalidArg,
348 format!(
349 "could not bind {}: {err}. Another process is probably already listening \
350 there; pass a different local port.",
351 spec.bind
352 ),
353 )
354 })
355}
356
357pub async fn hop_headers(
362 auth: &Arc<ProxyAuth>,
363 guest_port: u16,
364) -> Result<Vec<(String, String)>, Error> {
365 auth.headers_for_port(guest_port).await
366}
367
368pub async fn accept(listener: &TcpListener) -> Result<(TcpStream, SocketAddr), Error> {
373 listener.accept().await.map_err(|err| {
374 Error::new(
375 ErrorKind::Unexpected,
376 format!("the local listener stopped accepting: {err}"),
377 )
378 })
379}
380
381pub fn is_upgrade(status: u16) -> bool {
388 status == 101
389}
390
391pub trait LocalStream: AsyncRead + AsyncWrite + Unpin + Send {}
398impl<T: AsyncRead + AsyncWrite + Unpin + Send> LocalStream for T {}
399
400pub async fn serve_connection<S, F>(
416 local: S,
417 spec: &ForwardSpec,
418 auth: &Arc<ProxyAuth>,
419 client: &ForwardClient,
420 on_event: F,
421) -> Result<(), Error>
422where
423 S: LocalStream,
424 F: FnMut(ForwardEvent),
425{
426 serve_connection_via(local, spec, Some(auth), client, on_event).await
427}
428
429pub(crate) async fn serve_connection_via<S, F>(
436 local: S,
437 spec: &ForwardSpec,
438 auth: Option<&Arc<ProxyAuth>>,
439 client: &ForwardClient,
440 mut on_event: F,
441) -> Result<(), Error>
442where
443 S: LocalStream,
444 F: FnMut(ForwardEvent),
445{
446 let (events, mut pending_events) = mpsc::unbounded_channel();
447 let upgraded = Mutex::new(None);
448 let answered = AtomicBool::new(false);
449 let service = service_fn(|request| {
450 let result = forward_request(request, spec, auth, client, &events, &upgraded);
451 let answered = &answered;
452 Box::pin(async move {
453 let response = result.await;
454 if response.is_ok() {
455 answered.store(true, Ordering::Relaxed);
456 }
457 response
458 })
459 });
460 let mut builder = hyper::server::conn::http1::Builder::new();
461 builder
462 .timer(TokioTimer::new())
463 .header_read_timeout(HEADER_TIMEOUT)
464 .max_buf_size(MAX_REQUEST_HEAD_BYTES);
465 let mut connection = builder.serve_connection(TokioIo::new(local), service);
468 let result = {
469 let polling = futures_util::future::poll_fn(|cx| connection.poll_without_shutdown(cx));
470 tokio::pin!(polling);
471 loop {
472 tokio::select! {
473 result = &mut polling => break result,
474 Some(event) = pending_events.recv() => on_event(event),
475 }
476 }
477 };
478 while let Ok(event) = pending_events.try_recv() {
479 on_event(event);
480 }
481 let parts = connection.into_parts();
482 if let Err(error) = result {
483 let idle =
486 error.is_timeout() && answered.load(Ordering::Relaxed) && parts.read_buf.is_empty();
487 if !idle {
488 return Err(Error::new(
489 ErrorKind::Protocol,
490 format!("the local HTTP connection failed: {error}"),
491 ));
492 }
493 }
494 let upstream = upgraded.lock().expect("upgrade lock poisoned").take();
495 if let Some(mut upstream) = upstream {
496 let (read, write) = tokio::io::split(parts.io.into_inner());
499 let read = tokio::io::AsyncReadExt::chain(std::io::Cursor::new(parts.read_buf), read);
500 let mut local = tokio::io::join(read, write);
501 splice(&mut local, &mut upstream).await?;
502 }
503 Ok(())
504}
505
506const HEADER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
509const MAX_REQUEST_BODY_BYTES: usize = 16 * 1024 * 1024;
510
511type ForwardBody = UnsyncBoxBody<Bytes, Error>;
512
513fn full_body(body: Bytes) -> ForwardBody {
514 Full::new(body)
515 .map_err(|never| match never {})
516 .boxed_unsync()
517}
518
519fn local_response(status: u16, text: String) -> http::Response<ForwardBody> {
520 let mut response = http::Response::new(full_body(Bytes::from(text)));
521 *response.status_mut() =
522 http::StatusCode::from_u16(status).unwrap_or(http::StatusCode::BAD_GATEWAY);
523 response.headers_mut().insert(
524 http::header::CONTENT_TYPE,
525 http::HeaderValue::from_static("text/plain; charset=utf-8"),
526 );
527 response.headers_mut().insert(
528 http::header::CONNECTION,
529 http::HeaderValue::from_static("close"),
530 );
531 response
532}
533
534fn strip_hop_headers(headers: &mut http::HeaderMap, upgrade: bool) {
537 let nominated = headers
538 .get_all(http::header::CONNECTION)
539 .iter()
540 .filter_map(|value| value.to_str().ok())
541 .flat_map(|value| value.split(','))
542 .filter_map(|name| http::HeaderName::from_bytes(name.trim().as_bytes()).ok())
543 .collect::<Vec<_>>();
544 for name in nominated {
545 if !upgrade || name != http::header::UPGRADE {
546 headers.remove(name);
547 }
548 }
549 for name in [
550 "connection",
551 "keep-alive",
552 "proxy-connection",
553 "transfer-encoding",
554 "te",
555 "trailer",
556 "upgrade",
557 ] {
558 if !upgrade || (name != "connection" && name != "upgrade") {
559 headers.remove(name);
560 }
561 }
562 if upgrade {
563 headers.insert(
564 http::header::CONNECTION,
565 http::HeaderValue::from_static("upgrade"),
566 );
567 }
568}
569
570async fn forward_request(
571 request: http::Request<Incoming>,
572 spec: &ForwardSpec,
573 auth: Option<&Arc<ProxyAuth>>,
574 client: &ForwardClient,
575 events: &mpsc::UnboundedSender<ForwardEvent>,
576 upgraded: &Mutex<Option<reqwest::Upgraded>>,
577) -> Result<http::Response<ForwardBody>, Error> {
578 let headers = request
579 .headers()
580 .get_all(http::header::HOST)
581 .iter()
582 .map(|value| ("host".to_string(), value.to_str().unwrap_or("").to_string()))
583 .collect::<Vec<_>>();
584 if !local_host_matches(&headers, spec.bind) {
585 let explanation = "the request Host must name the local forward and its port".to_string();
586 let _ = events.send(ForwardEvent::Refused {
587 status: 403,
588 explanation: explanation.clone(),
589 });
590 return Ok(local_response(403, explanation));
591 }
592 if request
593 .headers()
594 .get(http::header::CONTENT_LENGTH)
595 .and_then(|value| value.to_str().ok())
596 .and_then(|value| value.parse::<u64>().ok())
597 .is_some_and(|length| length > MAX_REQUEST_BODY_BYTES as u64)
598 {
599 return Ok(local_response(
600 413,
601 "the local request body exceeds 16 MiB".into(),
602 ));
603 }
604 let (mut head, body) = request.into_parts();
605 let body = match tokio::time::timeout(
606 HEADER_TIMEOUT,
607 Limited::new(body, MAX_REQUEST_BODY_BYTES).collect(),
608 )
609 .await
610 {
611 Ok(Ok(body)) => body.to_bytes(),
612 Ok(Err(error)) if error.is::<http_body_util::LengthLimitError>() => {
613 return Ok(local_response(
614 413,
615 "the local request body exceeds 16 MiB".into(),
616 ));
617 }
618 Ok(Err(_)) => {
619 return Ok(local_response(
620 400,
621 "the local request body is malformed".into(),
622 ));
623 }
624 Err(_) => {
625 return Ok(local_response(
626 408,
627 "the local request body timed out".into(),
628 ));
629 }
630 };
631 let target = head
632 .uri
633 .path_and_query()
634 .map_or("/", |target| target.as_str());
635 let (minted, url) = match auth {
637 Some(auth) => (
638 hop_headers(auth, spec.guest_port).await?,
639 upstream_url(&spec.endpoint, target),
640 ),
641 None => (
642 Vec::new(),
643 direct_url(&spec.endpoint, spec.guest_port, target)?,
644 ),
645 };
646 let offered_upgrade = head.headers.contains_key(http::header::UPGRADE);
647 strip_hop_headers(&mut head.headers, offered_upgrade);
648 let mut request = client.inner.request(head.method, &url);
649 for (name, value) in &head.headers {
650 if forwards_request_header(name.as_str())
652 && name != http::header::TRANSFER_ENCODING
653 && name != http::header::CONTENT_LENGTH
654 {
655 request = request.header(name, value);
656 }
657 }
658 for (name, value) in &minted {
659 request = request.header(name, value);
660 }
661 let response = tokio::time::timeout(spec.exchange_timeout, request.body(body).send())
664 .await
665 .map_err(|_| Error::new(ErrorKind::Unexpected, "the guest's response head timed out"))?
666 .map_err(|err| {
667 Error::new(
668 ErrorKind::Unexpected,
669 format!("forwarding the request to the endpoint failed: {err}"),
670 )
671 })?;
672 let status = response.status();
673 if let Some(explanation) = refusal_explanation(status.as_u16(), spec.guest_port) {
674 let _ = events.send(ForwardEvent::Refused {
675 status: status.as_u16(),
676 explanation: explanation.clone(),
677 });
678 return Ok(local_response(status.as_u16(), explanation));
679 }
680 let mut headers = response.headers().clone();
681 let is_upgrade = is_upgrade(status.as_u16());
682 strip_hop_headers(&mut headers, is_upgrade);
683 let event = ForwardEvent::Forwarded {
684 status: status.as_u16(),
685 upgraded: is_upgrade,
686 };
687 let body = if is_upgrade {
688 let stream = response.upgrade().await.map_err(|err| {
689 Error::new(
690 ErrorKind::Unexpected,
691 format!("the endpoint answered 101 but did not upgrade: {err}"),
692 )
693 })?;
694 *upgraded.lock().expect("upgrade lock poisoned") = Some(stream);
695 let _ = events.send(event);
696 full_body(Bytes::new())
697 } else if response.content_length() == Some(0) {
698 let _ = events.send(event);
701 full_body(Bytes::new())
702 } else {
703 streaming_body(response, events.clone(), event, spec.exchange_timeout)
704 };
705 let mut response = http::Response::new(body);
706 *response.status_mut() = status;
707 *response.headers_mut() = headers;
708 Ok(response)
709}
710
711fn streaming_body(
714 response: reqwest::Response,
715 events: mpsc::UnboundedSender<ForwardEvent>,
716 event: ForwardEvent,
717 timeout: std::time::Duration,
718) -> ForwardBody {
719 let remaining = response.content_length();
720 let stream = response.bytes_stream().boxed();
721 let state = (stream, remaining, Some(event), events, timeout);
722 let frames = futures_util::stream::unfold(
723 state,
724 |(mut stream, mut remaining, mut event, events, timeout)| async move {
725 match tokio::time::timeout(timeout, stream.next()).await {
729 Ok(Some(Ok(chunk))) => {
730 if let Some(left) = remaining.as_mut() {
731 *left = left.saturating_sub(chunk.len() as u64);
732 if *left == 0
733 && let Some(event) = event.take()
734 {
735 let _ = events.send(event);
736 }
737 }
738 Some((
739 Ok(Frame::data(chunk)),
740 (stream, remaining, event, events, timeout),
741 ))
742 }
743 Ok(Some(Err(error))) => Some((
744 Err(Error::new(
745 ErrorKind::Unexpected,
746 format!("reading the guest's response body failed: {error}"),
747 )),
748 (stream, remaining, None, events, timeout),
749 )),
750 Err(_) => Some((
751 Err(Error::new(
752 ErrorKind::Unexpected,
753 "reading the guest's response body timed out",
754 )),
755 (stream, remaining, None, events, timeout),
756 )),
757 Ok(None) => {
758 if let Some(event) = event {
759 let _ = events.send(event);
760 }
761 None
762 }
763 }
764 },
765 );
766 StreamBody::new(frames).boxed_unsync()
767}
768
769const MAX_REQUEST_HEAD_BYTES: usize = 64 * 1024;
772
773pub async fn write_local_error<W>(stream: &mut W, status: u16, body: &str) -> Result<(), Error>
779where
780 W: AsyncWrite + Unpin + ?Sized,
781{
782 let reader =
785 std::io::Cursor::new(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
786 let io = tokio::io::join(reader, stream);
787 let service = service_fn(|_| {
788 std::future::ready(Ok::<_, std::convert::Infallible>(local_response(
789 status,
790 body.to_string(),
791 )))
792 });
793 hyper::server::conn::http1::Builder::new()
794 .half_close(true)
795 .serve_connection(TokioIo::new(io), service)
796 .await
797 .map_err(|err| {
798 Error::new(
799 ErrorKind::Unexpected,
800 format!("could not answer the local client: {err}"),
801 )
802 })
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808 use microvms_app::session::proxy::DEFAULT_AGENT_PORT;
809 use microvms_app::testing::{CountingMinter, TestClock};
810 use tokio::io::AsyncWriteExt as _;
811
812 struct RequestHead {
814 method: String,
815 target: String,
816 headers: Vec<(String, String)>,
817 }
818
819 async fn read_request_head<S: AsyncRead + Unpin>(
820 local: &mut S,
821 ) -> Result<Option<RequestHead>, std::io::Error> {
822 use tokio::io::AsyncReadExt as _;
823 let mut raw = Vec::new();
824 while !raw.ends_with(b"\r\n\r\n") {
825 let byte = match local.read_u8().await {
826 Ok(byte) => byte,
827 Err(err) if raw.is_empty() && err.kind() == std::io::ErrorKind::UnexpectedEof => {
828 return Ok(None);
829 }
830 Err(err) => return Err(err),
831 };
832 raw.push(byte);
833 }
834 let text = String::from_utf8_lossy(&raw);
835 let mut lines = text.split("\r\n");
836 let mut first = lines.next().unwrap().split_whitespace();
837 let method = first.next().unwrap().to_string();
838 let target = first.next().unwrap().to_string();
839 let headers: Vec<(String, String)> = lines
840 .filter_map(|line| line.split_once(':'))
841 .map(|(name, value)| (name.to_string(), value.trim().to_string()))
842 .collect();
843 let length = headers
844 .iter()
845 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
846 .and_then(|(_, value)| value.parse().ok())
847 .unwrap_or(0);
848 let mut body = vec![0; length];
849 local.read_exact(&mut body).await?;
850 Ok(Some(RequestHead {
851 method,
852 target,
853 headers,
854 }))
855 }
856
857 fn auth_pair() -> (Arc<ProxyAuth>, Arc<CountingMinter>) {
860 let minter = Arc::new(CountingMinter::default());
861 let auth = ProxyAuth::with_refresh_after(
862 minter.clone(),
863 DEFAULT_AGENT_PORT,
864 std::time::Duration::from_secs(30 * 60),
865 Arc::new(TestClock::default()),
866 )
867 .expect("the default refresh interval is below the ceiling");
868 (Arc::new(auth), minter)
869 }
870
871 async fn upstream(response: &'static str) -> (String, tokio::task::JoinHandle<String>) {
878 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
879 let addr = listener.local_addr().expect("bound");
880 let handle = tokio::spawn(async move {
881 let (mut socket, _) =
884 tokio::time::timeout(std::time::Duration::from_secs(10), listener.accept())
885 .await
886 .expect("the forwarder reaches the upstream")
887 .expect("one connection");
888 let head = read_request_head(&mut socket)
889 .await
890 .expect("a readable request")
891 .expect("a request arrived");
892 socket
893 .write_all(response.as_bytes())
894 .await
895 .expect("the canned response is written");
896 socket.flush().await.expect("flushed");
897 let headers = head
898 .headers
899 .iter()
900 .map(|(name, value)| format!("{}: {value}", name.to_ascii_lowercase()))
901 .collect::<Vec<_>>()
902 .join("\n");
903 format!("{} {}\n{headers}", head.method, head.target)
904 });
905 (format!("http://{addr}"), handle)
906 }
907
908 async fn exchange(
911 request: &str,
912 endpoint: String,
913 guest_port: u16,
914 auth: &Arc<ProxyAuth>,
915 ) -> (String, Vec<ForwardEvent>) {
916 let (mut client, server) = tokio::io::duplex(64 * 1024);
917 let spec = ForwardSpec::new(
918 "127.0.0.1:8080".parse().expect("an addr"),
919 guest_port,
920 endpoint,
921 );
922 let http = ForwardClient::new().expect("a client");
923
924 let auth = auth.clone();
925 let served = tokio::spawn(async move {
926 let mut events = Vec::new();
927 let result =
928 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
929 (result, events)
930 });
931
932 client
933 .write_all(
934 request
935 .replacen("HTTP/1.1\r\n", "HTTP/1.1\r\nconnection: close\r\n", 1)
936 .as_bytes(),
937 )
938 .await
939 .expect("the request is written");
940 client.flush().await.expect("flushed");
941
942 let (result, events) = served.await.expect("the task joins");
943 result.expect("the exchange completes");
944
945 let mut seen = Vec::new();
946 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut seen)
947 .await
948 .expect("the client reads the response");
949 (String::from_utf8_lossy(&seen).to_string(), events)
950 }
951
952 #[tokio::test]
953 async fn requests_name_the_local_listener_before_minting() {
954 for host in [
955 "",
956 "example.com:8080",
957 "localhost:8081",
958 "127.0.0.1",
959 "localhost:8080@elsewhere.example",
960 "user@localhost:8080",
961 "localhost:8080/path",
962 "localhost:8080\r\nHost: localhost:8080",
963 ] {
964 let (auth, _) = auth_pair();
965 let request = if host.is_empty() {
966 "GET / HTTP/1.1\r\n\r\n".to_string()
967 } else {
968 format!("GET / HTTP/1.1\r\nHost: {host}\r\n\r\n")
969 };
970 let (response, events) =
971 exchange(&request, "http://127.0.0.1:1".to_string(), 8080, &auth).await;
972 assert!(
973 response.starts_with("HTTP/1.1 403 Forbidden"),
974 "{host}: {response}"
975 );
976 assert_eq!(auth.mint_count(), 0, "{host}");
977 assert!(matches!(
978 events.as_slice(),
979 [ForwardEvent::Refused { status: 403, .. }]
980 ));
981 }
982 }
983
984 #[test]
985 fn host_authorities_match_the_bound_address_and_port() {
986 let bind = "127.0.0.1:8080".parse().expect("an address");
987 for host in [
988 "localhost:8080",
989 "LOCALHOST:8080",
990 "127.0.0.1:8080",
991 "[::1]:8080",
992 ] {
993 assert!(
994 local_host_matches(&[("Host".into(), host.into())], bind),
995 "{host}"
996 );
997 }
998 assert!(local_host_matches(
999 &[("Host".into(), "localhost".into())],
1000 "127.0.0.1:80".parse().unwrap()
1001 ));
1002 for host in ["localhost", "localhost:8081", "localhost:0"] {
1003 assert!(!local_host_matches(&[("Host".into(), host.into())], bind));
1004 }
1005 assert!(!local_host_matches(
1006 &[("Host".into(), "localhost:invalid".into())],
1007 bind
1008 ));
1009 assert!(!local_host_matches(
1010 &[("Host".into(), "localhost:65536".into())],
1011 bind
1012 ));
1013 assert!(!local_host_matches(
1014 &[("Host".into(), "localhost:65536".into())],
1015 "127.0.0.1:80".parse().unwrap()
1016 ));
1017 assert!(!local_host_matches(
1018 &[("Host".into(), "localhost:8080".into())],
1019 "192.0.2.1:8080".parse().unwrap()
1020 ));
1021 assert!(local_host_matches(
1022 &[("Host".into(), "192.0.2.1:8080".into())],
1023 "192.0.2.1:8080".parse().unwrap()
1024 ));
1025 }
1026
1027 #[tokio::test]
1028 async fn an_ordinary_get_reaches_the_guest_carrying_both_minted_headers() {
1029 let (endpoint, upstream_saw) =
1032 upstream("HTTP/1.1 200 OK\r\ncontent-length: 5\r\n\r\nhello").await;
1033 let (auth, minter) = auth_pair();
1034
1035 let (seen, events) = exchange(
1036 "GET /index.html HTTP/1.1\r\nhost: localhost:8080\r\nuser-agent: probe\r\n\r\n",
1037 endpoint,
1038 8080,
1039 &auth,
1040 )
1041 .await;
1042
1043 let request = upstream_saw.await.expect("the upstream task joins");
1044 assert!(request.starts_with("GET /index.html"), "{request}");
1045 assert!(
1046 request.contains(&format!(
1047 "{}: jwe-0",
1048 PROXY_AUTH_HEADER.to_ascii_lowercase()
1049 )),
1050 "the minted auth header did not reach upstream: {request}"
1051 );
1052 assert!(
1053 request.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
1054 "the port header did not name the guest port: {request}"
1055 );
1056 assert!(request.contains("user-agent: probe"), "{request}");
1057
1058 assert!(seen.starts_with("HTTP/1.1 200 OK\r\n"), "{seen}");
1059 assert!(seen.ends_with("hello"), "{seen}");
1060 assert!(
1061 matches!(
1062 events.as_slice(),
1063 [ForwardEvent::Forwarded {
1064 status: 200,
1065 upgraded: false
1066 }]
1067 ),
1068 "{events:?}"
1069 );
1070
1071 let requested = minter.requested();
1074 assert!(
1075 requested.iter().any(|ports| ports.contains(&8080)),
1076 "the mint never asked for port 8080: {requested:?}"
1077 );
1078 }
1079
1080 #[tokio::test]
1084 async fn the_caller_cannot_smuggle_its_own_proxy_headers_upstream() {
1085 let (endpoint, upstream_saw) =
1086 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1087 let (auth, _minter) = auth_pair();
1088
1089 let request = format!(
1090 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n{PROXY_AUTH_HEADER}: forged-jwe\r\n\
1091 {PROXY_PORT_HEADER}: 1\r\n\r\n"
1092 );
1093 let (_seen, _events) = exchange(&request, endpoint, 8080, &auth).await;
1094
1095 let saw = upstream_saw.await.expect("the upstream task joins");
1096 assert!(
1097 !saw.contains("forged-jwe"),
1098 "a caller's forged credential was forwarded: {saw}"
1099 );
1100 assert!(
1101 saw.contains(&format!("{}: 8080", PROXY_PORT_HEADER.to_ascii_lowercase())),
1102 "the minted port header should be the only one: {saw}"
1103 );
1104 assert!(
1105 !saw.contains("host: localhost:8080"),
1106 "the local host header must not override the endpoint's: {saw}"
1107 );
1108 }
1109
1110 #[tokio::test]
1111 async fn a_scope_refusal_is_answered_with_the_sentence_that_names_it() {
1112 let (endpoint, upstream_saw) =
1115 upstream("HTTP/1.1 403 Forbidden\r\ncontent-length: 0\r\n\r\n").await;
1116 let (auth, _minter) = auth_pair();
1117
1118 let (seen, events) = exchange(
1119 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1120 endpoint,
1121 8080,
1122 &auth,
1123 )
1124 .await;
1125 let _ = upstream_saw.await;
1126
1127 assert!(seen.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{seen}");
1128 assert!(seen.contains("scope does not cover"), "{seen}");
1129 match events.as_slice() {
1130 [
1131 ForwardEvent::Refused {
1132 status: 403,
1133 explanation,
1134 },
1135 ] => {
1136 assert!(explanation.contains("8080"), "{explanation}");
1137 }
1138 other => panic!("expected one 403 refusal, got {other:?}"),
1139 }
1140 }
1141
1142 #[tokio::test]
1143 async fn a_dead_guest_server_is_reported_as_502_rather_than_as_an_auth_problem() {
1144 let (endpoint, upstream_saw) =
1145 upstream("HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\n\r\n").await;
1146 let (auth, _minter) = auth_pair();
1147
1148 let (seen, events) = exchange(
1149 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1150 endpoint,
1151 5432,
1152 &auth,
1153 )
1154 .await;
1155 let _ = upstream_saw.await;
1156
1157 assert!(seen.contains("nothing listening on port 5432"), "{seen}");
1158 assert!(
1159 matches!(
1160 events.as_slice(),
1161 [ForwardEvent::Refused { status: 502, .. }]
1162 ),
1163 "{events:?}"
1164 );
1165 }
1166
1167 #[tokio::test]
1168 async fn the_guests_own_404_is_relayed_and_not_reinterpreted() {
1169 let (endpoint, upstream_saw) =
1172 upstream("HTTP/1.1 404 Not Found\r\ncontent-length: 9\r\n\r\nno-route!").await;
1173 let (auth, _minter) = auth_pair();
1174
1175 let (seen, events) = exchange(
1176 "GET /nope HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1177 endpoint,
1178 8080,
1179 &auth,
1180 )
1181 .await;
1182 let _ = upstream_saw.await;
1183
1184 assert!(seen.starts_with("HTTP/1.1 404 Not Found\r\n"), "{seen}");
1185 assert!(seen.ends_with("no-route!"), "{seen}");
1186 assert!(
1187 matches!(
1188 events.as_slice(),
1189 [ForwardEvent::Forwarded { status: 404, .. }]
1190 ),
1191 "{events:?}"
1192 );
1193 }
1194
1195 #[tokio::test]
1196 async fn a_post_body_is_forwarded_and_the_response_length_is_re_derived() {
1197 let (endpoint, upstream_saw) = upstream(
1200 "HTTP/1.1 200 OK\r\ncontent-length: 3\r\ntransfer-encoding: identity\r\n\r\nack",
1201 )
1202 .await;
1203 let (auth, _minter) = auth_pair();
1204
1205 let (seen, _events) = exchange(
1206 "POST /submit HTTP/1.1\r\nhost: localhost:8080\r\ncontent-length: 7\r\n\r\npayload",
1207 endpoint,
1208 8080,
1209 &auth,
1210 )
1211 .await;
1212 let saw = upstream_saw.await.expect("the upstream task joins");
1213
1214 assert!(saw.starts_with("POST /submit"), "{saw}");
1215 assert!(saw.contains("content-length: 7"), "{saw}");
1216 assert_eq!(
1217 seen.matches("content-length:").count(),
1218 1,
1219 "exactly one framing header should be written: {seen}"
1220 );
1221 assert!(seen.contains("content-length: 3\r\n"), "{seen}");
1222 assert!(
1223 !seen.to_ascii_lowercase().contains("transfer-encoding"),
1224 "the upstream framing header must not be copied: {seen}"
1225 );
1226 assert!(seen.ends_with("ack"), "{seen}");
1227 }
1228
1229 #[tokio::test]
1230 async fn a_101_relays_the_handshake_and_then_stops_parsing() {
1231 let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1235 let addr = listener.local_addr().expect("bound");
1236 let guest = tokio::spawn(async move {
1237 let (mut socket, _) = listener.accept().await.expect("one connection");
1238 let _ = read_request_head(&mut socket)
1239 .await
1240 .expect("a request")
1241 .expect("present");
1242 socket
1243 .write_all(
1244 b"HTTP/1.1 101 Switching Protocols\r\nupgrade: websocket\r\n\
1245 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1246 )
1247 .await
1248 .expect("the handshake is written");
1249 socket.flush().await.expect("flushed");
1250 let mut frame = [0_u8; 5];
1252 tokio::io::AsyncReadExt::read_exact(&mut socket, &mut frame)
1253 .await
1254 .expect("the client's bytes arrive");
1255 socket
1256 .write_all(b"PONG")
1257 .await
1258 .expect("the answer is written");
1259 socket.flush().await.expect("flushed");
1260 frame.to_vec()
1261 });
1262
1263 let (auth, _minter) = auth_pair();
1264 let (mut client, server) = tokio::io::duplex(64 * 1024);
1265 let spec = ForwardSpec::new(
1266 "127.0.0.1:8080".parse().expect("an addr"),
1267 8080,
1268 format!("http://{addr}"),
1269 );
1270 let http = ForwardClient::new().expect("a client");
1271 let served = tokio::spawn(async move {
1272 let mut events = Vec::new();
1273 let result =
1274 serve_connection(server, &spec, &auth, &http, |event| events.push(event)).await;
1275 (result, events)
1276 });
1277
1278 client
1279 .write_all(
1280 b"GET /socket HTTP/1.1\r\nhost: localhost:8080\r\nupgrade: websocket\r\n\
1281 connection: upgrade\r\nsec-websocket-protocol: my-app-protocol\r\n\r\n",
1282 )
1283 .await
1284 .expect("the handshake request is written");
1285 client.flush().await.expect("flushed");
1286
1287 let mut head = Vec::new();
1290 let mut byte = [0_u8; 1];
1291 while !head.ends_with(b"\r\n\r\n") {
1292 tokio::io::AsyncReadExt::read_exact(&mut client, &mut byte)
1293 .await
1294 .expect("the handshake response arrives");
1295 head.push(byte[0]);
1296 }
1297 let head = String::from_utf8_lossy(&head).to_string();
1298 assert!(
1299 head.starts_with("HTTP/1.1 101 Switching Protocols\r\n"),
1300 "{head}"
1301 );
1302 assert!(
1303 head.to_ascii_lowercase()
1304 .contains("sec-websocket-protocol: my-app-protocol"),
1305 "the guest's negotiated subprotocol must reach the client: {head}"
1306 );
1307
1308 client
1309 .write_all(b"PING!")
1310 .await
1311 .expect("a frame is written");
1312 client.flush().await.expect("flushed");
1313 let mut answer = [0_u8; 4];
1314 tokio::io::AsyncReadExt::read_exact(&mut client, &mut answer)
1315 .await
1316 .expect("the guest's answer arrives");
1317 assert_eq!(&answer, b"PONG");
1318
1319 drop(client);
1320 let guest_saw = guest.await.expect("the guest task joins");
1321 assert_eq!(
1322 &guest_saw, b"PING!",
1323 "the client's bytes reached the guest unframed"
1324 );
1325
1326 let (result, events) = served.await.expect("the task joins");
1327 result.expect("the splice ends cleanly");
1328 assert!(
1329 events.iter().any(|event| matches!(
1330 event,
1331 ForwardEvent::Forwarded {
1332 status: 101,
1333 upgraded: true
1334 }
1335 )),
1336 "{events:?}"
1337 );
1338 }
1339
1340 #[tokio::test]
1341 async fn every_exchange_mints_through_the_cache_rather_than_capturing_headers_once() {
1342 let (auth, minter) = auth_pair();
1346
1347 for _ in 0..2 {
1348 let (endpoint, upstream_saw) =
1349 upstream("HTTP/1.1 200 OK\r\ncontent-length: 2\r\n\r\nok").await;
1350 let (_seen, _events) = exchange(
1351 "GET / HTTP/1.1\r\nhost: localhost:8080\r\n\r\n",
1352 endpoint,
1353 8080,
1354 &auth,
1355 )
1356 .await;
1357 let saw = upstream_saw.await.expect("the upstream task joins");
1358 assert!(
1359 saw.contains(&PROXY_AUTH_HEADER.to_ascii_lowercase()),
1360 "every hop carries the minted credential: {saw}"
1361 );
1362 }
1363
1364 assert_eq!(
1368 auth.mint_count(),
1369 1,
1370 "the second hop should reuse the cached token rather than re-mint"
1371 );
1372 assert_eq!(minter.requested().len(), 1);
1373 }
1374
1375 #[test]
1376 fn only_a_101_counts_as_an_upgrade() {
1377 assert!(is_upgrade(101));
1381 assert!(!is_upgrade(200));
1382 assert!(!is_upgrade(426));
1383 }
1384
1385 #[test]
1386 fn a_bare_endpoint_host_is_read_as_https() {
1387 assert_eq!(
1390 upstream_url("vm-abc.example.aws", "/index.html"),
1391 "https://vm-abc.example.aws/index.html"
1392 );
1393 }
1394
1395 #[test]
1396 fn an_explicit_scheme_is_left_alone() {
1397 assert_eq!(
1398 upstream_url("https://vm-abc.example.aws/", "/a"),
1399 "https://vm-abc.example.aws/a"
1400 );
1401 assert_eq!(
1402 upstream_url("http://127.0.0.1:9000", "/a"),
1403 "http://127.0.0.1:9000/a"
1404 );
1405 }
1406
1407 #[test]
1408 fn a_target_without_a_leading_slash_still_forms_one_path() {
1409 assert_eq!(
1410 upstream_url("vm.example", "index.html"),
1411 "https://vm.example/index.html"
1412 );
1413 }
1414
1415 #[test]
1416 fn the_two_proxy_headers_are_never_copied_from_the_caller() {
1417 assert!(!forwards_request_header("x-aws-proxy-auth"));
1420 assert!(!forwards_request_header("X-AWS-PROXY-AUTH"));
1421 assert!(!forwards_request_header("x-aws-proxy-port"));
1422 assert!(!forwards_request_header("Host"));
1423 assert!(forwards_request_header("user-agent"));
1424 assert!(forwards_request_header("sec-websocket-protocol"));
1425 }
1426
1427 #[test]
1428 fn the_refusal_wording_separates_a_scope_mistake_from_a_dead_server() {
1429 let scope = refusal_explanation(403, 8080).expect("403 explains itself");
1432 assert!(scope.contains("scope does not cover"), "{scope}");
1433 assert!(scope.contains("8080"), "{scope}");
1434
1435 let dead = refusal_explanation(502, 8080).expect("502 explains itself");
1436 assert!(dead.contains("nothing listening"), "{dead}");
1437 assert!(dead.contains("8080"), "{dead}");
1438
1439 assert!(
1440 refusal_explanation(401, 8080).is_some(),
1441 "401 is worth naming"
1442 );
1443 assert!(
1444 refusal_explanation(200, 8080).is_none(),
1445 "a success explains nothing"
1446 );
1447 assert!(
1448 refusal_explanation(404, 8080).is_none(),
1449 "the guest's own 404 is not ours"
1450 );
1451 }
1452
1453 #[tokio::test]
1454 async fn a_bind_collision_names_the_address_and_suggests_another_port() {
1455 let held = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
1456 let addr = held.local_addr().expect("a bound address");
1457
1458 let spec = ForwardSpec::new(addr, 8080, "vm.example");
1459 let error = bind(&spec).await.expect_err("the address is taken");
1460 assert_eq!(error.kind(), ErrorKind::InvalidArg);
1461 assert!(error.to_string().contains(&addr.to_string()), "{error}");
1462 assert!(
1463 error.to_string().contains("different local port"),
1464 "{error}"
1465 );
1466 }
1467
1468 #[tokio::test]
1469 async fn a_local_error_response_is_a_readable_http_message() {
1470 let (mut client, mut server) = tokio::io::duplex(4096);
1473 let body = refusal_explanation(403, 8080).expect("wording");
1474 write_local_error(&mut server, 403, &body)
1475 .await
1476 .expect("the response is written");
1477 drop(server);
1478
1479 let mut got = Vec::new();
1480 tokio::io::AsyncReadExt::read_to_end(&mut client, &mut got)
1481 .await
1482 .expect("the client reads it");
1483 let text = String::from_utf8(got).expect("utf-8");
1484 assert!(text.starts_with("HTTP/1.1 403 Forbidden\r\n"), "{text}");
1485 assert!(text.contains("content-length: "), "{text}");
1486 assert!(text.ends_with(&body), "{text}");
1487 }
1488
1489 #[tokio::test]
1490 async fn splice_copies_both_directions_and_reports_the_byte_counts() {
1491 let (mut left_near, mut left_far) = tokio::io::duplex(4096);
1492 let (mut right_near, mut right_far) = tokio::io::duplex(4096);
1493
1494 let pump = tokio::spawn(async move { splice(&mut left_far, &mut right_near).await });
1495
1496 left_near.write_all(b"to-guest").await.expect("write");
1497 left_near.shutdown().await.expect("half close");
1498 right_far.write_all(b"from-guest!").await.expect("write");
1499 right_far.shutdown().await.expect("half close");
1500
1501 let mut to_guest = Vec::new();
1502 tokio::io::AsyncReadExt::read_to_end(&mut right_far, &mut to_guest)
1503 .await
1504 .expect("read");
1505 let mut from_guest = Vec::new();
1506 tokio::io::AsyncReadExt::read_to_end(&mut left_near, &mut from_guest)
1507 .await
1508 .expect("read");
1509
1510 let (a_to_b, b_to_a) = pump.await.expect("the task joins").expect("a clean splice");
1511 assert_eq!(to_guest, b"to-guest");
1512 assert_eq!(from_guest, b"from-guest!");
1513 assert_eq!(a_to_b, 8);
1514 assert_eq!(b_to_a, 11);
1515 }
1516
1517 #[test]
1518 fn hop_headers_preserve_only_negotiated_upgrades() {
1519 for upgrade in [false, true] {
1520 let mut headers = http::HeaderMap::new();
1521 for (name, value) in [
1522 ("connection", "x-private, upgrade"),
1523 ("upgrade", "example"),
1524 ("x-private", "one hop only"),
1525 ("x-end-to-end", "preserved"),
1526 ("keep-alive", "timeout=5"),
1527 ("proxy-connection", "keep-alive"),
1528 ("transfer-encoding", "chunked"),
1529 ("te", "trailers"),
1530 ("trailer", "x-checksum"),
1531 ] {
1532 headers.insert(
1533 http::HeaderName::from_static(name),
1534 http::HeaderValue::from_static(value),
1535 );
1536 }
1537 strip_hop_headers(&mut headers, upgrade);
1538 assert_eq!(headers["x-end-to-end"], "preserved");
1539 for name in [
1540 "x-private",
1541 "keep-alive",
1542 "proxy-connection",
1543 "transfer-encoding",
1544 "te",
1545 "trailer",
1546 ] {
1547 assert!(
1548 !headers.contains_key(name),
1549 "{name} crossed a hop (upgrade={upgrade})"
1550 );
1551 }
1552 if upgrade {
1553 assert_eq!(headers["connection"], "upgrade");
1554 assert_eq!(headers["upgrade"], "example");
1555 } else {
1556 assert!(!headers.contains_key("connection"));
1557 assert!(!headers.contains_key("upgrade"));
1558 }
1559 }
1560 }
1561}