1use std::borrow::Cow;
9use std::io;
10use std::net::{IpAddr, SocketAddr};
11use std::sync::Arc;
12use std::time::Duration;
13
14use bytes::Bytes;
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::TcpStream;
17use tokio::sync::mpsc;
18
19use super::connection::ProxyConnectState;
20#[cfg(test)]
21use super::connection::ProxyConnectStatus;
22use super::upstream::UpstreamTcpTarget;
23use crate::engine::http_deny::classify_http_request;
24use crate::engine::secrets::config::SecretsConfigExt;
25use crate::engine::tls::proxy::TlsProxy;
26use crate::engine::tls::sni;
27use crate::engine::tls::state::TlsState;
28use crate::netstack::shared::SharedState;
29use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
30use crate::proxy::ResolvedOutboundProxy;
31use crate::secrets::config::{SecretViolationAction, SecretsConfig};
32use crate::secrets::handler::{
33 SecretsHandler, first_line_is_not_http_request, looks_like_http_request_prefix,
34};
35
36const SERVER_READ_BUF_SIZE: usize = 16384;
42
43const CONNECT_RESP_LIMIT: usize = 8192;
45
46pub(crate) const PEEK_BUF_SIZE: usize = 16384;
48
49pub(crate) const PEEK_BUDGET: Duration = Duration::from_secs(5);
52
53#[derive(Debug)]
58struct ConnectRequest {
59 bytes: Vec<u8>,
60 header_end: usize,
61 target: ConnectTarget,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65struct ConnectTarget {
66 host: String,
67 port: u16,
68 expected_sni: Option<String>,
69}
70
71struct FirstFlight {
73 bytes: Vec<u8>,
74 is_tls: bool,
75 upstream_response_started: bool,
76}
77
78pub(crate) struct TcpProxy {
80 upstream: Option<TcpStream>,
82 guest_dst: SocketAddr,
83 connect_target: UpstreamTcpTarget,
84 from_smoltcp: mpsc::Receiver<Bytes>,
85 to_smoltcp: mpsc::Sender<Bytes>,
86 shared: Arc<SharedState>,
87 network_policy: Arc<NetworkPolicy>,
88 secrets: Arc<SecretsConfig>,
89 tls_state: Option<Arc<TlsState>>,
90 strict: bool,
91 proxy_connect: Arc<ProxyConnectState>,
92 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
93}
94
95impl ConnectRequest {
100 fn header_bytes(&self) -> &[u8] {
101 &self.bytes[..self.header_end]
102 }
103
104 fn post_header_bytes(&self) -> &[u8] {
105 &self.bytes[self.header_end..]
106 }
107}
108
109impl ConnectTarget {
110 fn is_intercepted(&self, tls_state: &TlsState) -> bool {
111 tls_state.config.intercepted_ports.contains(&self.port)
112 }
113
114 fn guest_dst(&self, fallback: SocketAddr, shared: &SharedState) -> SocketAddr {
115 if let Ok(ip) = self.host.parse::<IpAddr>() {
116 return SocketAddr::new(ip, self.port);
117 }
118
119 if self.host.eq_ignore_ascii_case(crate::HOST_ALIAS) {
120 match fallback.ip() {
121 IpAddr::V4(_) => {
122 if let Some(ip) = shared.gateway_ipv4() {
123 return SocketAddr::new(IpAddr::V4(ip), self.port);
124 }
125 }
126 IpAddr::V6(_) => {
127 if let Some(ip) = shared.gateway_ipv6() {
128 return SocketAddr::new(IpAddr::V6(ip), self.port);
129 }
130 }
131 }
132 if let Some(ip) = shared.gateway_ipv4() {
133 return SocketAddr::new(IpAddr::V4(ip), self.port);
134 }
135 if let Some(ip) = shared.gateway_ipv6() {
136 return SocketAddr::new(IpAddr::V6(ip), self.port);
137 }
138 }
139
140 SocketAddr::new(fallback.ip(), self.port)
141 }
142}
143
144impl TcpProxy {
145 #[allow(clippy::too_many_arguments)]
149 pub(crate) fn new(
150 upstream: Option<TcpStream>,
151 guest_dst: SocketAddr,
152 connect_target: UpstreamTcpTarget,
153 from_smoltcp: mpsc::Receiver<Bytes>,
154 to_smoltcp: mpsc::Sender<Bytes>,
155 shared: Arc<SharedState>,
156 network_policy: Arc<NetworkPolicy>,
157 secrets: Arc<SecretsConfig>,
158 tls_state: Option<Arc<TlsState>>,
159 strict: bool,
160 proxy_connect: Arc<ProxyConnectState>,
161 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
162 ) -> Self {
163 Self {
164 upstream,
165 guest_dst,
166 connect_target,
167 from_smoltcp,
168 to_smoltcp,
169 shared,
170 network_policy,
171 secrets,
172 tls_state,
173 strict,
174 proxy_connect,
175 outbound_proxy,
176 }
177 }
178
179 pub(crate) async fn run(self) {
181 let guest_dst = self.guest_dst;
182 let connect_dst = self.connect_target.primary();
183
184 if let Err(error) = self.try_run().await {
185 tracing::debug!(
186 dst = %connect_dst,
187 %guest_dst,
188 %error,
189 "TCP proxy task ended",
190 );
191 }
192 }
193
194 async fn try_run(self) -> io::Result<()> {
196 let Self {
197 upstream,
198 guest_dst,
199 connect_target,
200 mut from_smoltcp,
201 to_smoltcp,
202 shared,
203 network_policy,
204 secrets,
205 tls_state,
206 strict,
207 proxy_connect,
208 outbound_proxy,
209 } = self;
210
211 let hostname_policy_deferred = match network_policy.evaluate_egress_with_source(
215 guest_dst,
216 Protocol::Tcp,
217 &shared,
218 HostnameSource::Deferred,
219 ) {
220 EgressEvaluation::Allow => false,
221 EgressEvaluation::DeferUntilHostname => true,
222 EgressEvaluation::Deny => network_policy.has_domain_rules(),
225 };
226
227 let peek_started = tokio::time::Instant::now();
233 let (mut initial_buf, sni) = if hostname_policy_deferred {
234 peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await
235 } else {
236 (Vec::new(), None)
237 };
238
239 if hostname_policy_deferred {
245 let source = match sni.as_deref() {
246 Some(name) => HostnameSource::Sni(name),
247 None => HostnameSource::CacheOnly,
248 };
249 match network_policy.evaluate_egress_with_source(
250 guest_dst,
251 Protocol::Tcp,
252 &shared,
253 source,
254 ) {
255 EgressEvaluation::Allow => {
256 if strict_hostname_allow_is_opaque(
257 strict,
258 &network_policy,
259 guest_dst,
260 &shared,
261 sni.as_deref(),
262 &initial_buf,
263 ) {
264 tracing::debug!(
265 sni = sni.as_deref(),
266 dst = %guest_dst,
267 "TCP egress denied by strict hostname policy",
268 );
269 proxy_connect.mark_policy_denied();
270 shared.proxy_wake.wake();
271 return Ok(());
272 }
273 }
274 EgressEvaluation::Deny => {
275 tracing::debug!(
276 dst = %guest_dst,
277 source = source.label(),
278 "TCP egress denied by domain policy",
279 );
280 if shared.http_deny_response_enabled() {
281 initial_buf = peek_for_http_request(
282 &mut from_smoltcp,
283 initial_buf,
284 PEEK_BUF_SIZE,
285 PEEK_BUDGET.saturating_sub(peek_started.elapsed()),
286 )
287 .await;
288 }
289 return deny_http_or_close(
290 guest_dst,
291 sni.as_deref(),
292 &initial_buf,
293 to_smoltcp,
294 &shared,
295 &proxy_connect,
296 )
297 .await;
298 }
299 EgressEvaluation::DeferUntilHostname => {
300 debug_assert!(false, "DeferUntilHostname leaked into TCP proxy task");
301 if shared.http_deny_response_enabled() {
302 initial_buf = peek_for_http_request(
303 &mut from_smoltcp,
304 initial_buf,
305 PEEK_BUF_SIZE,
306 PEEK_BUDGET.saturating_sub(peek_started.elapsed()),
307 )
308 .await;
309 }
310 return deny_http_or_close(
311 guest_dst,
312 sni.as_deref(),
313 &initial_buf,
314 to_smoltcp,
315 &shared,
316 &proxy_connect,
317 )
318 .await;
319 }
320 }
321 }
322
323 if let Some(tls_state) = tls_state.clone()
327 && !initial_buf.is_empty()
328 && could_be_connect_request(&initial_buf)
329 {
330 return handle_connect_tunnel(
331 guest_dst,
332 connect_target,
333 initial_buf,
334 from_smoltcp,
335 to_smoltcp,
336 shared,
337 network_policy,
338 tls_state,
339 strict,
340 proxy_connect,
341 outbound_proxy,
342 upstream,
343 )
344 .await;
345 }
346
347 let stream = match upstream {
352 Some(stream) => stream,
353 None => {
354 connect_target
355 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
356 .await?
357 }
358 };
359 let connect_dst = stream.peer_addr().unwrap_or(connect_target.primary());
360 let (mut server_rx, mut server_tx) = stream.into_split();
361
362 let enforce_http_authority = network_policy.has_domain_rules();
368 let want_headers = enforce_http_authority
369 || secrets.has_plain_http_candidates()
370 || secrets.has_host_scoped_secrets();
371
372 let FirstFlight {
373 bytes: initial_buf,
374 is_tls,
375 mut upstream_response_started,
376 } = if want_headers {
377 classify_first_flight(
378 initial_buf,
379 &mut from_smoltcp,
380 &mut server_rx,
381 &to_smoltcp,
382 &shared,
383 want_headers,
384 PEEK_BUF_SIZE,
385 PEEK_BUDGET,
386 )
387 .await?
388 } else {
389 FirstFlight {
390 bytes: initial_buf,
391 is_tls: false,
392 upstream_response_started: false,
393 }
394 };
395
396 if let Some(tls_state) = tls_state.clone()
397 && could_be_connect_request(&initial_buf)
398 {
399 let proxy_stream = server_rx
404 .reunite(server_tx)
405 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
406 return handle_connect_tunnel(
407 guest_dst,
408 connect_target,
409 initial_buf,
410 from_smoltcp,
411 to_smoltcp,
412 shared,
413 network_policy,
414 tls_state,
415 strict,
416 proxy_connect,
417 outbound_proxy,
418 Some(proxy_stream),
419 )
420 .await;
421 }
422
423 let mut late_connect_state = tls_state;
424 let mut secrets_handler: Option<SecretsHandler> = if is_tls {
425 None
426 } else if enforce_http_authority {
427 let host = extract_http_host(&initial_buf).unwrap_or_default();
428 Some(SecretsHandler::new_plain_http_policy(
429 &secrets,
430 &host,
431 guest_dst,
432 network_policy.clone(),
433 shared.clone(),
434 ))
435 } else if !secrets.secrets.is_empty() {
436 Some(match extract_http_host(&initial_buf) {
437 Some(host) => {
438 SecretsHandler::new_plain_http(&secrets, &host, guest_dst.ip(), &shared)
439 }
440 None => SecretsHandler::new_plain_http_invalid_host(&secrets),
441 })
442 } else {
443 None
444 };
445
446 if !initial_buf.is_empty() {
448 let out: Cow<[u8]> = match secrets_handler.as_mut() {
449 Some(h) => match h.substitute(&initial_buf) {
450 Ok(cow) => cow,
453 Err(action) => {
454 if matches!(action, SecretViolationAction::BlockAndTerminate) {
455 shared.trigger_termination();
456 } else if shared.secret_deny_response_enabled()
457 && !upstream_response_started
458 && let Some(response) =
459 h.http1_violation_response(shared.secret_deny_message())
460 {
461 let _ = to_smoltcp.send(Bytes::from(response)).await;
462 shared.proxy_wake.wake();
463 }
464 return Ok(());
465 }
466 },
467 None => Cow::Borrowed(&initial_buf),
468 };
469 if !out.is_empty() {
470 if let Err(e) = server_tx.write_all(&out).await {
471 tracing::debug!(dst = %connect_dst, error = %e, "replay of buffered first flight failed");
472 return Ok(());
473 }
474 if let Err(e) = server_tx.flush().await {
475 tracing::debug!(dst = %connect_dst, error = %e, "flush after first flight failed");
476 return Ok(());
477 }
478 }
479 }
480
481 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
482
483 let mut guest_eof = false;
488 loop {
489 tokio::select! {
490 data = from_smoltcp.recv(), if !guest_eof => {
492 match data {
493 Some(bytes) => {
494 if let Some(tls_state) = late_connect_state.take()
495 && could_be_connect_request(&bytes)
496 {
497 let proxy_stream = server_rx
502 .reunite(server_tx)
503 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
504 return handle_connect_tunnel(
505 guest_dst,
506 connect_target,
507 bytes.to_vec(),
508 from_smoltcp,
509 to_smoltcp,
510 shared,
511 network_policy,
512 tls_state,
513 strict,
514 proxy_connect,
515 outbound_proxy,
516 Some(proxy_stream),
517 )
518 .await;
519 }
520 let out: Cow<[u8]> = match secrets_handler.as_mut() {
523 Some(h) => match h.substitute(&bytes) {
524 Ok(cow) => cow,
525 Err(action) => {
526 if matches!(action, SecretViolationAction::BlockAndTerminate)
527 {
528 shared.trigger_termination();
529 } else if shared.secret_deny_response_enabled()
530 && !upstream_response_started
531 && let Some(response) = h.http1_violation_response(shared.secret_deny_message())
532 {
533 let _ = to_smoltcp.send(Bytes::from(response)).await;
534 shared.proxy_wake.wake();
535 }
536 break;
537 }
538 },
539 None => Cow::Borrowed(&bytes),
540 };
541 if !out.is_empty() {
542 if let Err(e) = server_tx.write_all(&out).await {
543 tracing::debug!(dst = %connect_dst, error = %e, "write to server failed");
544 break;
545 }
546 if let Err(e) = server_tx.flush().await {
547 tracing::debug!(dst = %connect_dst, error = %e, "flush to server failed");
548 break;
549 }
550 }
551 }
552 None => {
557 guest_eof = true;
558 if server_tx.shutdown().await.is_err() {
559 break;
560 }
561 }
562 }
563 }
564
565 result = server_rx.read(&mut server_buf) => {
567 match result {
568 Ok(0) => break, Ok(n) => {
570 upstream_response_started = true;
571
572 late_connect_state = None;
575 let data = Bytes::copy_from_slice(&server_buf[..n]);
576 if to_smoltcp.send(data).await.is_err() {
577 break;
579 }
580 shared.proxy_wake.wake();
583 }
584 Err(e) => {
585 tracing::debug!(dst = %connect_dst, error = %e, "read from server failed");
586 break;
587 }
588 }
589 }
590 }
591 }
592
593 Ok(())
594 }
595}
596
597#[allow(clippy::too_many_arguments)]
610pub fn spawn_tcp_proxy(
611 handle: &tokio::runtime::Handle,
612 guest_dst: SocketAddr,
613 connect_dst: SocketAddr,
614 from_smoltcp: mpsc::Receiver<Bytes>,
615 to_smoltcp: mpsc::Sender<Bytes>,
616 shared: Arc<SharedState>,
617 network_policy: Arc<NetworkPolicy>,
618 secrets: Arc<SecretsConfig>,
619 tls_state: Option<Arc<TlsState>>,
620 strict: bool,
621 proxy_connect: Arc<ProxyConnectState>,
622 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
623) {
624 let proxy = TcpProxy::new(
625 None,
626 guest_dst,
627 UpstreamTcpTarget::direct(connect_dst),
628 from_smoltcp,
629 to_smoltcp,
630 shared,
631 network_policy,
632 secrets,
633 tls_state,
634 strict,
635 proxy_connect,
636 outbound_proxy,
637 );
638
639 handle.spawn(proxy.run());
640}
641
642fn strict_hostname_allow_is_opaque(
643 strict: bool,
644 network_policy: &NetworkPolicy,
645 guest_dst: SocketAddr,
646 shared: &SharedState,
647 sni: Option<&str>,
648 initial_buf: &[u8],
649) -> bool {
650 if !strict {
651 return false;
652 }
653
654 let source = if let Some(name) = sni {
655 HostnameSource::Sni(name)
656 } else if initial_buf.is_empty() || initial_buf.first() == Some(&0x16) {
657 HostnameSource::CacheOnly
658 } else {
659 return false;
660 };
661
662 network_policy.allows_egress_via_hostname(guest_dst, Protocol::Tcp, shared, source)
663}
664
665#[allow(clippy::too_many_arguments)]
671async fn handle_connect_tunnel(
672 guest_dst: SocketAddr,
673 proxy_target: UpstreamTcpTarget,
674 initial_buf: Vec<u8>,
675 mut from_smoltcp: mpsc::Receiver<Bytes>,
676 to_smoltcp: mpsc::Sender<Bytes>,
677 shared: Arc<SharedState>,
678 network_policy: Arc<NetworkPolicy>,
679 tls_state: Arc<TlsState>,
680 strict: bool,
681 proxy_connect: Arc<ProxyConnectState>,
682 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
683 preconnected_proxy: Option<TcpStream>,
684) -> io::Result<()> {
685 let connect_req =
686 parse_connect_request(buffer_connect_request(initial_buf, &mut from_smoltcp).await?)?;
687
688 let connect_headers =
689 match sanitize_connect_headers(connect_req.header_bytes(), &tls_state.secrets.load()) {
690 Ok(headers) => headers,
691 Err(action) => {
692 if matches!(action, SecretViolationAction::BlockAndTerminate) {
693 shared.trigger_termination();
694 }
695 return Ok(());
696 }
697 };
698
699 let mut proxy_stream = match preconnected_proxy {
701 Some(stream) => stream,
702 None => {
703 proxy_target
704 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
705 .await?
706 }
707 };
708
709 if !connect_req.target.is_intercepted(&tls_state) {
710 let tunnel_dst = connect_req.target.guest_dst(guest_dst, &shared);
711 if strict
712 && let Some(expected_sni) = connect_req.target.expected_sni.as_deref()
713 && network_policy.allows_egress_via_hostname(
714 tunnel_dst,
715 Protocol::Tcp,
716 &shared,
717 HostnameSource::Sni(expected_sni),
718 )
719 {
720 tracing::debug!(
721 sni = %expected_sni,
722 dst = %tunnel_dst,
723 "CONNECT tunnel denied by strict hostname policy",
724 );
725 proxy_connect.mark_policy_denied();
726 shared.proxy_wake.wake();
727 return Ok(());
728 }
729 proxy_stream.write_all(&connect_headers).await?;
730 proxy_stream.flush().await?;
731 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
732 if to_smoltcp
733 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
734 .await
735 .is_err()
736 {
737 return Ok(());
738 }
739 if !proxy_resp[header_end..].is_empty()
740 && to_smoltcp
741 .send(Bytes::copy_from_slice(&proxy_resp[header_end..]))
742 .await
743 .is_err()
744 {
745 return Ok(());
746 }
747 shared.proxy_wake.wake();
748 if !connect_response_is_success(&proxy_resp[..header_end]) {
749 proxy_connect.mark_connected();
750 return Ok(());
751 }
752 if !connect_req.post_header_bytes().is_empty() {
753 proxy_stream
754 .write_all(connect_req.post_header_bytes())
755 .await?;
756 }
757 proxy_stream.flush().await?;
758 proxy_connect.mark_connected();
759 return relay_connected_stream(proxy_stream, from_smoltcp, to_smoltcp, shared).await;
760 }
761
762 proxy_stream.write_all(&connect_headers).await?;
763 proxy_stream.flush().await?;
764
765 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
766 if !connect_response_is_success(&proxy_resp[..header_end]) {
767 return Err(io::Error::new(
768 io::ErrorKind::ConnectionRefused,
769 "proxy rejected CONNECT",
770 ));
771 }
772 if !proxy_resp[header_end..].is_empty() {
773 return Err(io::Error::new(
774 io::ErrorKind::InvalidData,
775 "proxy sent unexpected bytes after CONNECT response headers",
776 ));
777 }
778 proxy_connect.mark_connected();
779
780 if to_smoltcp
781 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
782 .await
783 .is_err()
784 {
785 return Ok(());
786 }
787 shared.proxy_wake.wake();
788
789 let tls_seed = connect_req.post_header_bytes().to_vec();
790 let tls_guest_dst = connect_req.target.guest_dst(guest_dst, &shared);
791 let expected_sni = connect_req.target.expected_sni.clone();
792
793 TlsProxy::new(
794 Some(proxy_stream),
795 tls_guest_dst,
796 proxy_target,
797 from_smoltcp,
798 to_smoltcp,
799 shared,
800 tls_state,
801 network_policy,
802 strict,
803 proxy_connect,
804 None,
806 )
807 .with_expected_sni(expected_sni)
808 .with_initial_buf(tls_seed)
809 .try_run()
810 .await
811}
812
813async fn relay_connected_stream(
815 stream: TcpStream,
816 mut from_smoltcp: mpsc::Receiver<Bytes>,
817 to_smoltcp: mpsc::Sender<Bytes>,
818 shared: Arc<SharedState>,
819) -> io::Result<()> {
820 let (mut server_rx, mut server_tx) = stream.into_split();
821 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
822
823 let mut guest_eof = false;
824 loop {
825 tokio::select! {
826 data = from_smoltcp.recv(), if !guest_eof => {
827 match data {
828 Some(bytes) => {
829 server_tx.write_all(&bytes).await?;
830 server_tx.flush().await?;
831 }
832 None => {
835 guest_eof = true;
836 if server_tx.shutdown().await.is_err() {
837 break;
838 }
839 }
840 }
841 }
842 result = server_rx.read(&mut server_buf) => {
843 match result {
844 Ok(0) => break,
845 Ok(n) => {
846 if to_smoltcp
847 .send(Bytes::copy_from_slice(&server_buf[..n]))
848 .await
849 .is_err()
850 {
851 break;
852 }
853 shared.proxy_wake.wake();
854 }
855 Err(e) => return Err(e),
856 }
857 }
858 }
859 }
860
861 Ok(())
862}
863
864async fn buffer_connect_request(
865 mut buf: Vec<u8>,
866 from_smoltcp: &mut mpsc::Receiver<Bytes>,
867) -> io::Result<Vec<u8>> {
868 let timeout_fut = tokio::time::sleep(PEEK_BUDGET);
869 tokio::pin!(timeout_fut);
870
871 loop {
872 if !could_be_connect_request(&buf) {
873 return Err(io::Error::new(
874 io::ErrorKind::InvalidData,
875 "malformed CONNECT request prefix",
876 ));
877 }
878 if headers_end(&buf).is_some() {
879 return Ok(buf);
880 }
881 if buf.len() >= PEEK_BUF_SIZE {
882 return Err(io::Error::new(
883 io::ErrorKind::InvalidData,
884 "CONNECT request headers too large",
885 ));
886 }
887
888 tokio::select! {
889 biased;
890 _ = &mut timeout_fut => {
891 return Err(io::Error::new(
892 io::ErrorKind::TimedOut,
893 "timed out waiting for complete CONNECT request headers",
894 ));
895 }
896 data = from_smoltcp.recv() => match data {
897 Some(bytes) => {
898 buf.extend_from_slice(&bytes);
899 }
900 None => {
901 return Err(io::Error::new(
902 io::ErrorKind::UnexpectedEof,
903 "channel closed before complete CONNECT request headers",
904 ));
905 }
906 }
907 }
908 }
909}
910
911async fn read_connect_response_headers(stream: &mut TcpStream) -> io::Result<(Vec<u8>, usize)> {
912 tokio::time::timeout(PEEK_BUDGET, async {
913 let mut proxy_resp = Vec::with_capacity(256);
914 let mut buf = [0u8; 4096];
915 loop {
916 let n = stream.read(&mut buf).await?;
917 if n == 0 {
918 return Err(io::Error::new(
919 io::ErrorKind::UnexpectedEof,
920 "proxy closed before sending CONNECT response",
921 ));
922 }
923 proxy_resp.extend_from_slice(&buf[..n]);
924 if let Some(end) = headers_end(&proxy_resp) {
925 return Ok((proxy_resp, end));
926 }
927 if proxy_resp.len() > CONNECT_RESP_LIMIT {
928 return Err(io::Error::new(
929 io::ErrorKind::InvalidData,
930 "proxy CONNECT response too large",
931 ));
932 }
933 }
934 })
935 .await
936 .map_err(|_| {
937 io::Error::new(
938 io::ErrorKind::TimedOut,
939 "timed out waiting for proxy CONNECT response",
940 )
941 })?
942}
943
944fn sanitize_connect_headers<'a>(
945 header_bytes: &'a [u8],
946 secrets: &SecretsConfig,
947) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
948 if secrets.secrets.is_empty() {
949 return Ok(Cow::Borrowed(header_bytes));
950 }
951
952 let mut handler = SecretsHandler::new_plain_http_untrusted_metadata(secrets);
953 handler.substitute(header_bytes)
954}
955
956fn headers_end(buf: &[u8]) -> Option<usize> {
958 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
959}
960
961fn could_be_connect_request(buf: &[u8]) -> bool {
962 const PREFIX: &[u8] = b"CONNECT ";
963 if buf.is_empty() {
964 return false;
965 }
966 let n = buf.len().min(PREFIX.len());
967 buf[..n].eq_ignore_ascii_case(&PREFIX[..n])
968}
969
970fn parse_connect_request(bytes: Vec<u8>) -> io::Result<ConnectRequest> {
971 let header_end = headers_end(&bytes).ok_or_else(|| {
972 io::Error::new(
973 io::ErrorKind::InvalidData,
974 "incomplete CONNECT request headers",
975 )
976 })?;
977 let target = {
978 let request_line = bytes[..header_end]
979 .split(|&b| b == b'\n')
980 .next()
981 .unwrap_or(&[]);
982 let request_line = std::str::from_utf8(request_line)
983 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "CONNECT line is not UTF-8"))?
984 .trim_end_matches('\r');
985 let mut parts = request_line.split_ascii_whitespace();
986 let method = parts.next().unwrap_or_default();
987 let authority = parts.next().unwrap_or_default();
988 let version = parts.next().unwrap_or_default();
989 if !method.eq_ignore_ascii_case("CONNECT")
990 || authority.is_empty()
991 || !is_http_version(version)
992 || parts.next().is_some()
993 {
994 return Err(io::Error::new(
995 io::ErrorKind::InvalidData,
996 "malformed CONNECT request line",
997 ));
998 }
999 parse_connect_target(authority)?
1000 };
1001
1002 Ok(ConnectRequest {
1003 bytes,
1004 header_end,
1005 target,
1006 })
1007}
1008
1009fn parse_connect_target(authority: &str) -> io::Result<ConnectTarget> {
1010 let authority = authority.trim();
1011 let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
1012 let (host, rest) = rest.split_once(']').ok_or_else(|| {
1013 io::Error::new(
1014 io::ErrorKind::InvalidData,
1015 "malformed CONNECT IPv6 authority",
1016 )
1017 })?;
1018 let port = rest.strip_prefix(':').ok_or_else(|| {
1019 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
1020 })?;
1021 (host, port)
1022 } else {
1023 let (host, port) = authority.rsplit_once(':').ok_or_else(|| {
1024 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
1025 })?;
1026 if host.contains(':') {
1027 return Err(io::Error::new(
1028 io::ErrorKind::InvalidData,
1029 "CONNECT IPv6 authority must be bracketed",
1030 ));
1031 }
1032 (host, port)
1033 };
1034 let host = host.trim().trim_end_matches('.');
1035 if host.is_empty() {
1036 return Err(io::Error::new(
1037 io::ErrorKind::InvalidData,
1038 "CONNECT authority missing host",
1039 ));
1040 }
1041 let port = port
1042 .parse::<u16>()
1043 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid CONNECT port"))?;
1044 let expected_sni = host
1045 .parse::<IpAddr>()
1046 .is_err()
1047 .then(|| host.to_ascii_lowercase());
1048
1049 Ok(ConnectTarget {
1050 host: host.to_ascii_lowercase(),
1051 port,
1052 expected_sni,
1053 })
1054}
1055
1056fn is_http_version(version: &str) -> bool {
1057 let Some(version) = version.strip_prefix("HTTP/") else {
1058 return false;
1059 };
1060 let Some((major, minor)) = version.split_once('.') else {
1061 return false;
1062 };
1063 !major.is_empty()
1064 && !minor.is_empty()
1065 && major.bytes().all(|b| b.is_ascii_digit())
1066 && minor.bytes().all(|b| b.is_ascii_digit())
1067}
1068
1069fn connect_response_is_success(headers: &[u8]) -> bool {
1070 let Some(status_line) = headers.split(|&b| b == b'\n').next() else {
1071 return false;
1072 };
1073 let Ok(status_line) = std::str::from_utf8(status_line) else {
1074 return false;
1075 };
1076 let mut parts = status_line.trim_end_matches('\r').split_ascii_whitespace();
1077 let version = parts.next().unwrap_or_default();
1078 let status = parts.next().unwrap_or_default();
1079 is_http_version(version)
1080 && status.len() == 3
1081 && status
1082 .parse::<u16>()
1083 .is_ok_and(|code| (200..300).contains(&code))
1084}
1085
1086pub(crate) async fn deny_http_or_close(
1092 guest_dst: SocketAddr,
1093 sni: Option<&str>,
1094 initial_buf: &[u8],
1095 to_smoltcp: mpsc::Sender<Bytes>,
1096 shared: &SharedState,
1097 proxy_connect: &ProxyConnectState,
1098) -> io::Result<()> {
1099 let answer = shared.http_deny_response_enabled() && first_flight_is_http(initial_buf);
1101 if answer {
1102 let host = denied_host_label(sni, initial_buf, guest_dst);
1103 let _ = to_smoltcp
1104 .send(Bytes::from(shared.network_denial_response(&host)))
1105 .await;
1106 shared.proxy_wake.wake();
1107 }
1108 proxy_connect.mark_policy_denied();
1109 shared.proxy_wake.wake();
1110 Ok(())
1111}
1112
1113fn first_flight_is_http(buf: &[u8]) -> bool {
1114 classify_http_request(buf) == Some(true)
1115}
1116
1117fn denied_host_label(sni: Option<&str>, buf: &[u8], guest_dst: SocketAddr) -> String {
1118 if let Some(name) = sni.filter(|name| !name.is_empty()) {
1119 return name.to_string();
1120 }
1121 if let Some(host) = extract_http_host(buf) {
1122 return host;
1123 }
1124 guest_dst.ip().to_string()
1125}
1126
1127fn extract_http_host(buf: &[u8]) -> Option<String> {
1137 if buf.first() == Some(&0x16) {
1138 return None;
1139 }
1140 let mut headers = vec![httparse::EMPTY_HEADER; (buf.len() / 4).max(16)];
1146 let mut req = httparse::Request::new(&mut headers);
1147 req.parse(buf).ok()?;
1148 req.headers
1149 .iter()
1150 .find(|h| h.name.eq_ignore_ascii_case("host"))
1151 .and_then(|h| std::str::from_utf8(h.value).ok())
1152 .map(|v| {
1153 let host = v.trim();
1154 host.rsplit_once(':')
1156 .map(|(h, _)| h)
1157 .unwrap_or(host)
1158 .to_ascii_lowercase()
1159 })
1160 .filter(|h| !h.is_empty())
1161}
1162
1163#[allow(clippy::too_many_arguments)]
1180async fn classify_first_flight(
1181 mut buf: Vec<u8>,
1182 from_smoltcp: &mut mpsc::Receiver<Bytes>,
1183 server_rx: &mut tokio::net::tcp::OwnedReadHalf,
1184 to_smoltcp: &mpsc::Sender<Bytes>,
1185 shared: &SharedState,
1186 want_headers: bool,
1187 max: usize,
1188 budget: Duration,
1189) -> io::Result<FirstFlight> {
1190 let mut upstream_response_started = false;
1191 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
1192 let timeout_fut = tokio::time::sleep(budget);
1193 tokio::pin!(timeout_fut);
1194
1195 loop {
1196 if !buf.is_empty() {
1202 let is_tls = buf.first() == Some(&0x16);
1203 let not_http = !is_tls
1204 && (!looks_like_http_request_prefix(&buf) || first_line_is_not_http_request(&buf));
1205 let done = !want_headers
1206 || is_tls
1207 || not_http
1208 || buf.len() >= max
1209 || buf.windows(4).any(|w| w == b"\r\n\r\n");
1210 if done {
1211 break;
1212 }
1213 }
1214
1215 tokio::select! {
1216 biased;
1217 _ = &mut timeout_fut => {
1218 break;
1219 }
1220 guest = from_smoltcp.recv() => match guest {
1223 Some(bytes) => buf.extend_from_slice(&bytes),
1224 None => {
1225 break;
1226 }
1227 },
1228 server = server_rx.read(&mut server_buf) => match server {
1231 Ok(0) => {
1232 break;
1233 }
1234 Ok(n) => {
1235 let data = Bytes::copy_from_slice(&server_buf[..n]);
1236 if to_smoltcp.send(data).await.is_err() {
1237 break;
1238 }
1239 upstream_response_started = true;
1240 shared.proxy_wake.wake();
1241 }
1242 Err(e) => return Err(e),
1243 },
1244 }
1245 }
1246
1247 Ok(FirstFlight {
1248 is_tls: buf.first() == Some(&0x16),
1249 bytes: buf,
1250 upstream_response_started,
1251 })
1252}
1253
1254pub(crate) async fn peek_for_http_request(
1263 rx: &mut mpsc::Receiver<Bytes>,
1264 mut buf: Vec<u8>,
1265 max: usize,
1266 budget: Duration,
1267) -> Vec<u8> {
1268 buf.truncate(max);
1269 let timeout_fut = tokio::time::sleep(budget);
1270 tokio::pin!(timeout_fut);
1271
1272 while buf.len() < max {
1273 match classify_http_request(&buf) {
1274 Some(false) => break,
1275 Some(true) => {
1276 let mut request = buf.as_slice();
1277 while let Some(rest) = request.strip_prefix(b"\r\n") {
1278 request = rest;
1279 }
1280 if request.windows(4).any(|bytes| bytes == b"\r\n\r\n") {
1281 break;
1282 }
1283 }
1284 None => {}
1285 }
1286 tokio::select! {
1287 biased;
1288 _ = &mut timeout_fut => break,
1289 data = rx.recv() => match data {
1290 Some(bytes) => {
1291 let remaining = max - buf.len();
1292 buf.extend_from_slice(&bytes[..bytes.len().min(remaining)]);
1293 }
1294 None => break,
1295 }
1296 }
1297 }
1298 buf
1299}
1300
1301pub(crate) async fn peek_for_sni(
1311 rx: &mut mpsc::Receiver<Bytes>,
1312 max: usize,
1313 budget: Duration,
1314) -> (Vec<u8>, Option<String>) {
1315 let mut buf = Vec::with_capacity(PEEK_BUF_SIZE.min(8192));
1316 let timeout_fut = tokio::time::sleep(budget);
1317 tokio::pin!(timeout_fut);
1318
1319 let raw_sni = loop {
1320 tokio::select! {
1321 biased;
1322 _ = &mut timeout_fut => break None,
1323 data = rx.recv() => {
1324 match data {
1325 Some(bytes) => {
1326 buf.extend_from_slice(&bytes);
1327 if buf.first() != Some(&0x16) {
1332 break None;
1333 }
1334 if let Some(name) = sni::extract_sni(&buf) {
1335 break Some(name);
1336 }
1337 if buf.len() >= max {
1338 break None;
1339 }
1340 }
1341 None => break None,
1342 }
1343 }
1344 }
1345 };
1346
1347 let canonical = raw_sni.map(|s| s.trim_end_matches('.').to_ascii_lowercase());
1348 (buf, canonical)
1349}
1350
1351#[cfg(test)]
1356mod tests {
1357 use super::*;
1358
1359 fn synthetic_client_hello(sni: &str) -> Vec<u8> {
1363 let host_bytes = sni.as_bytes();
1366 let host_len = host_bytes.len() as u16;
1367 let server_name_list_len = 3 + host_len; let extension_data_len = 2 + server_name_list_len; let extensions_total = 4 + extension_data_len; let mut body = Vec::new();
1372 body.extend_from_slice(&[0x03, 0x03]);
1374 body.extend_from_slice(&[0u8; 32]);
1376 body.push(0);
1378 body.extend_from_slice(&[0x00, 0x02, 0x00, 0x2f]);
1380 body.extend_from_slice(&[0x01, 0x00]);
1382 body.extend_from_slice(&extensions_total.to_be_bytes());
1384 body.extend_from_slice(&[0x00, 0x00]);
1386 body.extend_from_slice(&extension_data_len.to_be_bytes());
1387 body.extend_from_slice(&server_name_list_len.to_be_bytes());
1388 body.push(0x00); body.extend_from_slice(&host_len.to_be_bytes());
1390 body.extend_from_slice(host_bytes);
1391
1392 let handshake_len = body.len() as u32;
1393 let mut hs = Vec::new();
1394 hs.push(0x01); hs.extend_from_slice(&handshake_len.to_be_bytes()[1..]); hs.extend_from_slice(&body);
1397
1398 let record_len = hs.len() as u16;
1399 let mut record = Vec::new();
1400 record.extend_from_slice(&[0x16, 0x03, 0x01]); record.extend_from_slice(&record_len.to_be_bytes());
1402 record.extend_from_slice(&hs);
1403
1404 record
1405 }
1406
1407 #[tokio::test]
1408 async fn connect_upstream_dials_target_directly_without_outbound_proxy() {
1409 use tokio::net::TcpListener;
1410
1411 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1412 let addr = listener.local_addr().unwrap();
1413 let accept = tokio::spawn(async move {
1414 let (mut sock, _) = listener.accept().await.unwrap();
1415 let mut buf = [0u8; 5];
1416 sock.read_exact(&mut buf).await.unwrap();
1417 assert_eq!(&buf, b"hello");
1418 });
1419
1420 let shared = SharedState::new(4);
1421 let proxy_connect = ProxyConnectState::new();
1422 let mut stream = UpstreamTcpTarget::direct(addr)
1423 .connect(&proxy_connect, &shared, None)
1424 .await
1425 .unwrap();
1426 stream.write_all(b"hello").await.unwrap();
1427
1428 accept.await.unwrap();
1429 assert!(matches!(
1430 proxy_connect.status(),
1431 ProxyConnectStatus::Connected
1432 ));
1433 }
1434
1435 #[tokio::test]
1436 async fn early_http_connect_dials_proxy_through_configured_socks5_proxy() {
1437 let _ = rustls::crypto::ring::default_provider().install_default();
1438
1439 let http_proxy_addr: SocketAddr = "93.184.216.34:3128".parse().unwrap();
1442 let socks_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1443 let outbound_proxy = ResolvedOutboundProxy::Socks5 {
1444 address: socks_listener.local_addr().unwrap(),
1445 credentials: None,
1446 };
1447 let socks_task = tokio::spawn(async move {
1448 let (mut client, _) = socks_listener.accept().await.unwrap();
1449
1450 let mut greeting = [0u8; 3];
1451 client.read_exact(&mut greeting).await.unwrap();
1452 assert_eq!(greeting, [0x05, 0x01, 0x00]);
1453 client.write_all(&[0x05, 0x00]).await.unwrap();
1454
1455 let mut socks_request = [0u8; 10];
1456 client.read_exact(&mut socks_request).await.unwrap();
1457 assert_eq!(socks_request[0..4], [0x05, 0x01, 0x00, 0x01]);
1458 assert_eq!(&socks_request[4..8], &[93, 184, 216, 34]);
1459 assert_eq!(
1460 u16::from_be_bytes([socks_request[8], socks_request[9]]),
1461 3128
1462 );
1463 client
1464 .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1465 .await
1466 .unwrap();
1467
1468 let expected_connect =
1469 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
1470 let mut connect_request = vec![0u8; expected_connect.len()];
1471 client.read_exact(&mut connect_request).await.unwrap();
1472 assert_eq!(&connect_request, expected_connect);
1473 client
1474 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
1475 .await
1476 .unwrap();
1477 });
1478
1479 let connect_request =
1480 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n".to_vec();
1481 let (from_tx, from_rx) = mpsc::channel(1);
1482 let (to_tx, mut to_rx) = mpsc::channel(1);
1483 drop(from_tx);
1484
1485 let tls_state = Arc::new(
1486 TlsState::new(
1487 microsandbox_types::TlsConfig::default(),
1488 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1489 )
1490 .unwrap(),
1491 );
1492 let proxy_connect = Arc::new(ProxyConnectState::new());
1493
1494 handle_connect_tunnel(
1495 http_proxy_addr,
1496 UpstreamTcpTarget::direct(http_proxy_addr),
1497 connect_request,
1498 from_rx,
1499 to_tx,
1500 Arc::new(SharedState::new(4)),
1501 Arc::new(NetworkPolicy::default()),
1502 tls_state,
1503 false,
1504 proxy_connect.clone(),
1505 Some(Arc::new(outbound_proxy)),
1506 None,
1507 )
1508 .await
1509 .unwrap();
1510
1511 let response = to_rx.recv().await.unwrap();
1512 assert_eq!(
1513 &response[..],
1514 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1515 );
1516 socks_task.await.unwrap();
1517 assert!(matches!(
1518 proxy_connect.status(),
1519 ProxyConnectStatus::Connected
1520 ));
1521 }
1522
1523 #[test]
1524 fn could_be_connect_request_matches_split_prefixes_only() {
1525 assert!(could_be_connect_request(b"C"));
1526 assert!(could_be_connect_request(b"connect "));
1527 assert!(could_be_connect_request(b"CONNECT example.com:443"));
1528 assert!(!could_be_connect_request(b"CLIENT"));
1529 assert!(!could_be_connect_request(b"GET / HTTP/1.1\r\n"));
1530 }
1531
1532 #[test]
1533 fn first_flight_http_accepts_partial_and_complete_http() {
1534 assert!(!first_flight_is_http(b"GET /index.html"));
1535 assert!(first_flight_is_http(b"GET /x HTTP/1.1\r\nHost: a\r\n"));
1536 assert!(first_flight_is_http(b"\r\nGET /x HTTP/1.0\r\n"));
1537 assert!(!first_flight_is_http(b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"));
1539 assert!(first_flight_is_http(b"QUERY /x HTTP/1.1\r\n"));
1541 }
1542
1543 #[test]
1544 fn first_flight_http_rejects_split_non_http_banners() {
1545 assert!(!first_flight_is_http(b""));
1546 assert!(!first_flight_is_http(&synthetic_client_hello(
1547 "example.com"
1548 )));
1549 assert!(!first_flight_is_http(b"GE"));
1550 assert!(!first_flight_is_http(b"PRI"));
1551 assert!(!first_flight_is_http(b"SSH-2.0-OpenSSH_9.9"));
1554 assert!(!first_flight_is_http(b"EHLO mail.example.com"));
1555 assert!(!first_flight_is_http(b"QUERY /x"));
1556 }
1557
1558 #[tokio::test]
1559 async fn buffer_connect_request_reads_split_headers() {
1560 let (tx, mut rx) = mpsc::channel(4);
1561 tx.send(Bytes::from_static(b"NECT example.com:443 HTTP/1.1\r\n"))
1562 .await
1563 .unwrap();
1564 tx.send(Bytes::from_static(b"Host: example.com\r\n\r\n"))
1565 .await
1566 .unwrap();
1567 drop(tx);
1568
1569 let buffered = buffer_connect_request(b"CON".to_vec(), &mut rx)
1570 .await
1571 .unwrap();
1572 let parsed = parse_connect_request(buffered).unwrap();
1573
1574 assert_eq!(parsed.target.host, "example.com");
1575 assert_eq!(parsed.target.port, 443);
1576 assert_eq!(parsed.target.expected_sni.as_deref(), Some("example.com"));
1577 assert!(parsed.post_header_bytes().is_empty());
1578 }
1579
1580 #[test]
1581 fn parse_connect_request_preserves_post_header_tls_seed() {
1582 let mut request = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
1583 request.extend_from_slice(b"\x16\x03\x01client-hello");
1584
1585 let parsed = parse_connect_request(request).unwrap();
1586
1587 assert_eq!(
1588 parsed.header_bytes(),
1589 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n"
1590 );
1591 assert_eq!(parsed.post_header_bytes(), b"\x16\x03\x01client-hello");
1592 }
1593
1594 #[test]
1595 fn parse_connect_target_requires_authority_port() {
1596 assert!(parse_connect_target("example.com").is_err());
1597 assert!(parse_connect_target("2001:db8::1:443").is_err());
1598
1599 let target = parse_connect_target("[2001:db8::1]:8443").unwrap();
1600 assert_eq!(target.host, "2001:db8::1");
1601 assert_eq!(target.port, 8443);
1602 assert_eq!(target.expected_sni, None);
1603 }
1604
1605 #[test]
1606 fn connect_response_success_requires_exact_2xx_status_code() {
1607 assert!(connect_response_is_success(
1608 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1609 ));
1610 assert!(connect_response_is_success(
1611 b"HTTP/1.1 204 Connection Established\r\n\r\n"
1612 ));
1613 assert!(!connect_response_is_success(b"HTTP/1.1 2000 Weird\r\n\r\n"));
1614 assert!(!connect_response_is_success(b"HTTP/1.1 199 Nope\r\n\r\n"));
1615 assert!(!connect_response_is_success(b"NOTHTTP 200 OK\r\n\r\n"));
1616 }
1617
1618 #[tokio::test(start_paused = true)]
1619 async fn peek_for_http_request_joins_a_fragmented_method() {
1620 let (tx, mut rx) = mpsc::channel(4);
1621 tx.send(Bytes::from_static(b"GE")).await.unwrap();
1622 tx.send(Bytes::from_static(b"T / HTTP/1.1\r\nHost: x\r\n\r\n"))
1623 .await
1624 .unwrap();
1625 drop(tx);
1626
1627 let buf = peek_for_http_request(&mut rx, Vec::new(), PEEK_BUF_SIZE, PEEK_BUDGET).await;
1628 assert_eq!(buf, b"GET / HTTP/1.1\r\nHost: x\r\n\r\n");
1629 assert!(first_flight_is_http(&buf));
1630 }
1631
1632 #[tokio::test(start_paused = true)]
1633 async fn peek_for_http_request_joins_a_fragmented_non_http_line() {
1634 let (tx, mut rx) = mpsc::channel(4);
1635 tx.send(Bytes::from_static(b"EH")).await.unwrap();
1636 tx.send(Bytes::from_static(b"LO mail.example.com\r\n"))
1637 .await
1638 .unwrap();
1639 drop(tx);
1640
1641 let buf = peek_for_http_request(&mut rx, Vec::new(), PEEK_BUF_SIZE, PEEK_BUDGET).await;
1642 assert_eq!(buf, b"EHLO mail.example.com\r\n");
1643 assert!(!first_flight_is_http(&buf));
1644 }
1645
1646 #[tokio::test(start_paused = true)]
1647 async fn denied_http_peek_preserves_seed_and_split_leading_crlf() {
1648 let (tx, mut rx) = mpsc::channel(4);
1649 tx.send(Bytes::from_static(b"\nGE")).await.unwrap();
1650 tx.send(Bytes::from_static(b"T / HTTP/1.1\r"))
1651 .await
1652 .unwrap();
1653 tx.send(Bytes::from_static(b"\nHost: blocked.example\r\n\r\n"))
1654 .await
1655 .unwrap();
1656 drop(tx);
1657 let buf = peek_for_http_request(&mut rx, b"\r".to_vec(), PEEK_BUF_SIZE, PEEK_BUDGET).await;
1658 assert!(first_flight_is_http(&buf));
1659 assert_eq!(extract_http_host(&buf).as_deref(), Some("blocked.example"));
1660 }
1661
1662 #[tokio::test(start_paused = true)]
1663 async fn denied_http_peek_bounds_incomplete_requests() {
1664 let (tx, mut rx) = mpsc::channel(1);
1665 tx.send(Bytes::from_static(b"GET /an-overlong-request"))
1666 .await
1667 .unwrap();
1668 let buf = peek_for_http_request(&mut rx, Vec::new(), 8, PEEK_BUDGET).await;
1669 assert_eq!(buf.len(), 8);
1670 assert!(!first_flight_is_http(&buf));
1671 let buf = peek_for_http_request(
1672 &mut rx,
1673 b"GE".to_vec(),
1674 PEEK_BUF_SIZE,
1675 Duration::from_millis(1),
1676 )
1677 .await;
1678 assert_eq!(buf, b"GE");
1679 assert!(!first_flight_is_http(&buf));
1680 }
1681
1682 #[tokio::test]
1683 async fn domain_denial_joins_fragmented_request_without_dialing_upstream() {
1684 for (enabled, host_header, expected_domain) in [
1685 (false, "Host: blocked.example\r\n", "blocked.example"),
1686 (true, "Host: blocked.example\r\n", "blocked.example"),
1687 (true, "", "127.0.0.1"),
1688 ] {
1689 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1690 let dst = listener.local_addr().unwrap();
1691 let shared = Arc::new(shared_with("blocked.example", "127.0.0.1"));
1692 shared.set_http_config(microsandbox_types::HttpConfig {
1693 deny_response: enabled,
1694 deny_response_format: HttpDenyResponseFormat::Json,
1695 network_deny_message: Some("blocked {host}".into()),
1696 ..Default::default()
1697 });
1698 let policy = Arc::new(NetworkPolicy {
1699 default_egress: Action::Deny,
1700 default_ingress: Action::Allow,
1701 rules: vec![allow_tcp("allowed.example", dst.port())],
1702 });
1703 let status = Arc::new(ProxyConnectState::new());
1704 let (from_tx, from_rx) = mpsc::channel(4);
1705 let (to_tx, mut to_rx) = mpsc::channel(4);
1706 from_tx.send(Bytes::from_static(b"GE")).await.unwrap();
1707 from_tx
1708 .send(Bytes::from_static(b"T / HTTP/1.1\r\n"))
1709 .await
1710 .unwrap();
1711 from_tx
1712 .send(Bytes::from(format!("{host_header}\r\n")))
1713 .await
1714 .unwrap();
1715 drop(from_tx);
1716 TcpProxy::new(
1717 None,
1718 dst,
1719 UpstreamTcpTarget::direct(dst),
1720 from_rx,
1721 to_tx,
1722 shared,
1723 policy,
1724 Arc::new(SecretsConfig::default()),
1725 None,
1726 false,
1727 status.clone(),
1728 None,
1729 )
1730 .try_run()
1731 .await
1732 .unwrap();
1733 let response = to_rx.recv().await;
1734 if enabled {
1735 let response = response.unwrap();
1736 assert!(response.starts_with(b"HTTP/1.1 403 Forbidden\r\n"));
1737 let text = std::str::from_utf8(&response).unwrap();
1738 let (_, body) = text.split_once("\r\n\r\n").unwrap();
1739 let error: serde_json::Value = serde_json::from_str(body).unwrap();
1740 assert_eq!(error["code"], "network_policy_denied");
1741 assert_eq!(error["message"], "blocked {host}");
1743 assert_eq!(error["domain"], expected_domain);
1744 } else {
1745 assert!(response.is_none(), "disabled responses must close silently");
1746 }
1747 assert_eq!(status.status(), ProxyConnectStatus::PolicyDenied);
1748 assert!(
1749 tokio::time::timeout(Duration::from_millis(20), listener.accept())
1750 .await
1751 .is_err()
1752 );
1753 }
1754 }
1755
1756 #[tokio::test]
1757 async fn peek_for_sni_extracts_and_canonicalizes() {
1758 let (tx, mut rx) = mpsc::channel(4);
1759 let hello = synthetic_client_hello("Example.COM");
1760 tx.send(Bytes::from(hello.clone())).await.unwrap();
1761 drop(tx); let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1764 assert_eq!(sni.as_deref(), Some("example.com"));
1765 assert_eq!(buf, hello);
1766 }
1767
1768 #[tokio::test]
1769 async fn peek_for_sni_returns_none_on_channel_close_without_data() {
1770 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1771 drop(tx);
1772 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1773 assert!(buf.is_empty());
1774 assert_eq!(sni, None);
1775 }
1776
1777 #[tokio::test]
1778 async fn peek_for_sni_returns_none_on_non_tls_data() {
1779 let (tx, mut rx) = mpsc::channel(4);
1780 tx.send(Bytes::from_static(
1782 b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
1783 ))
1784 .await
1785 .unwrap();
1786 drop(tx);
1787 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1788 assert!(
1789 !buf.is_empty(),
1790 "buffered bytes must be returned for replay"
1791 );
1792 assert_eq!(sni, None);
1793 }
1794
1795 #[tokio::test]
1796 async fn peek_for_sni_falls_back_on_timeout() {
1797 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1798 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, Duration::from_millis(50)).await;
1800 drop(tx);
1801 assert!(buf.is_empty());
1802 assert_eq!(sni, None);
1803 }
1804
1805 #[tokio::test]
1806 async fn peek_for_sni_caps_at_max_bytes() {
1807 let (tx, mut rx) = mpsc::channel(4);
1808 let mut first = vec![0u8; 8192];
1812 first[0] = 0x16;
1813 tx.send(Bytes::from(first)).await.unwrap();
1814 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1815 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1816 drop(tx);
1817
1818 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1819 assert_eq!(sni, None, "no SNI in non-TLS data");
1820 assert!(
1821 buf.len() >= PEEK_BUF_SIZE,
1822 "buffer must hit the cap before bail-out: got {}",
1823 buf.len()
1824 );
1825 }
1826
1827 #[tokio::test]
1828 async fn peek_for_sni_bails_immediately_on_non_tls_first_byte() {
1829 let (tx, mut rx) = mpsc::channel(4);
1830 tx.send(Bytes::from_static(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
1832 .await
1833 .unwrap();
1834 drop(tx);
1835
1836 let started = std::time::Instant::now();
1839 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1840 let elapsed = started.elapsed();
1841 assert_eq!(sni, None);
1842 assert!(buf.starts_with(b"GET"));
1843 assert!(
1844 elapsed < Duration::from_millis(500),
1845 "non-TLS bail must be fast: took {elapsed:?}"
1846 );
1847 }
1848
1849 use std::net::IpAddr;
1854 use std::time::Duration as StdDuration;
1855
1856 use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
1857 use crate::policy::{Action, Destination, NetworkPolicy, PortRange, Rule};
1858
1859 const SHARED_FASTLY_IP: &str = "151.101.0.223";
1860
1861 fn shared_with(host: &str, ip: &str) -> SharedState {
1862 let shared = SharedState::new(4);
1863 shared.cache_resolved_hostname(
1864 host,
1865 ResolvedHostnameFamily::Ipv4,
1866 [ip.parse::<IpAddr>().unwrap()],
1867 StdDuration::from_secs(60),
1868 );
1869 shared
1870 }
1871
1872 fn allow_https(domain: &str) -> Rule {
1873 Rule {
1874 direction: crate::policy::Direction::Egress,
1875 destination: Destination::Domain(domain.parse().unwrap()),
1876 protocols: vec![Protocol::Tcp],
1877 ports: vec![PortRange::single(443)],
1878 action: Action::Allow,
1879 }
1880 }
1881
1882 fn allow_tcp(domain: &str, port: u16) -> Rule {
1883 Rule {
1884 direction: crate::policy::Direction::Egress,
1885 destination: Destination::Domain(domain.parse().unwrap()),
1886 protocols: vec![Protocol::Tcp],
1887 ports: vec![PortRange::single(port)],
1888 action: Action::Allow,
1889 }
1890 }
1891
1892 #[tokio::test]
1895 async fn integration_sni_overrides_cache_for_over_allow() {
1896 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1897 let policy = NetworkPolicy {
1898 default_egress: Action::Deny,
1899 default_ingress: Action::Allow,
1900 rules: vec![allow_https("pypi.org")],
1901 };
1902 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1903
1904 let (tx, mut rx) = mpsc::channel(4);
1905 tx.send(Bytes::from(synthetic_client_hello("evil.com")))
1906 .await
1907 .unwrap();
1908 drop(tx);
1909
1910 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1911 assert_eq!(sni.as_deref(), Some("evil.com"));
1912 assert!(!initial_buf.is_empty());
1913
1914 let source = sni
1915 .as_deref()
1916 .map(HostnameSource::Sni)
1917 .unwrap_or(HostnameSource::CacheOnly);
1918 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1919 assert_eq!(
1920 eval,
1921 EgressEvaluation::Deny,
1922 "SNI=evil.com must not piggy-back on the cached pypi.org match",
1923 );
1924 }
1925
1926 #[tokio::test]
1929 async fn integration_sni_overrides_cache_for_over_block() {
1930 let shared = shared_with("ads.example.com", SHARED_FASTLY_IP);
1931 let policy = NetworkPolicy {
1932 default_egress: Action::Allow,
1933 default_ingress: Action::Allow,
1934 rules: vec![Rule::deny_egress(Destination::Domain(
1935 "ads.example.com".parse().unwrap(),
1936 ))],
1937 };
1938 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1939
1940 let (tx, mut rx) = mpsc::channel(4);
1941 tx.send(Bytes::from(synthetic_client_hello("api.example.com")))
1942 .await
1943 .unwrap();
1944 drop(tx);
1945
1946 let (_initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1947 assert_eq!(sni.as_deref(), Some("api.example.com"));
1948
1949 let source = sni
1950 .as_deref()
1951 .map(HostnameSource::Sni)
1952 .unwrap_or(HostnameSource::CacheOnly);
1953 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1954 assert_eq!(
1955 eval,
1956 EgressEvaluation::Allow,
1957 "SNI=api.example.com must not be caught by the deny on ads.example.com",
1958 );
1959 }
1960
1961 #[tokio::test]
1964 async fn integration_non_tls_falls_back_to_cache() {
1965 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1966 let policy = NetworkPolicy {
1967 default_egress: Action::Deny,
1968 default_ingress: Action::Allow,
1969 rules: vec![allow_https("pypi.org")],
1970 };
1971 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1972
1973 let (tx, mut rx) = mpsc::channel(4);
1974 tx.send(Bytes::from_static(
1976 b"GET / HTTP/1.1\r\nHost: pypi.org\r\n\r\n",
1977 ))
1978 .await
1979 .unwrap();
1980 drop(tx);
1981
1982 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1983 assert_eq!(sni, None, "non-TLS data → no SNI");
1984 assert!(
1985 !initial_buf.is_empty(),
1986 "buffered bytes must survive for replay"
1987 );
1988
1989 let source = sni
1990 .as_deref()
1991 .map(HostnameSource::Sni)
1992 .unwrap_or(HostnameSource::CacheOnly);
1993 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1994 assert_eq!(
1995 eval,
1996 EgressEvaluation::Allow,
1997 "cache-only fallback must still allow the cached hostname's IP",
1998 );
1999 }
2000
2001 #[tokio::test]
2004 async fn integration_sni_matches_domain_suffix_with_cache_binding() {
2005 let shared = shared_with("files.pythonhosted.org", SHARED_FASTLY_IP);
2006 let policy = NetworkPolicy {
2007 default_egress: Action::Deny,
2008 default_ingress: Action::Allow,
2009 rules: vec![Rule {
2010 direction: crate::policy::Direction::Egress,
2011 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
2012 protocols: vec![Protocol::Tcp],
2013 ports: vec![PortRange::single(443)],
2014 action: Action::Allow,
2015 }],
2016 };
2017 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
2018
2019 let (tx, mut rx) = mpsc::channel(4);
2020 tx.send(Bytes::from(synthetic_client_hello(
2021 "files.pythonhosted.org",
2022 )))
2023 .await
2024 .unwrap();
2025 drop(tx);
2026
2027 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
2028 let source = sni
2029 .as_deref()
2030 .map(HostnameSource::Sni)
2031 .unwrap_or(HostnameSource::CacheOnly);
2032 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
2033 assert_eq!(eval, EgressEvaluation::Allow);
2034 }
2035
2036 #[tokio::test]
2041 async fn integration_sni_denies_domain_suffix_without_cache_binding() {
2042 let shared = SharedState::new(4); let policy = NetworkPolicy {
2044 default_egress: Action::Deny,
2045 default_ingress: Action::Allow,
2046 rules: vec![Rule {
2047 direction: crate::policy::Direction::Egress,
2048 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
2049 protocols: vec![Protocol::Tcp],
2050 ports: vec![PortRange::single(443)],
2051 action: Action::Allow,
2052 }],
2053 };
2054 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
2055
2056 let (tx, mut rx) = mpsc::channel(4);
2057 tx.send(Bytes::from(synthetic_client_hello(
2058 "files.pythonhosted.org",
2059 )))
2060 .await
2061 .unwrap();
2062 drop(tx);
2063
2064 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
2065 let source = sni
2066 .as_deref()
2067 .map(HostnameSource::Sni)
2068 .unwrap_or(HostnameSource::CacheOnly);
2069 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
2070 assert_eq!(eval, EgressEvaluation::Deny);
2071 }
2072
2073 #[test]
2076 fn extract_http_host_basic() {
2077 let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
2078 assert_eq!(extract_http_host(buf), Some("example.com".into()));
2079 }
2080
2081 #[test]
2082 fn extract_http_host_strips_port() {
2083 let buf = b"POST /api HTTP/1.1\r\nHost: api.company.com:8080\r\n\r\n";
2084 assert_eq!(extract_http_host(buf), Some("api.company.com".into()));
2085 }
2086
2087 #[test]
2088 fn extract_http_host_case_insensitive_lowercased() {
2089 let buf = b"GET / HTTP/1.1\r\nhost: Example.COM\r\n\r\n";
2090 assert_eq!(extract_http_host(buf), Some("example.com".into()));
2091 }
2092
2093 #[test]
2094 fn extract_http_host_no_host_header() {
2095 let buf = b"GET / HTTP/1.1\r\nX-Other: foo\r\n\r\n";
2096 assert_eq!(extract_http_host(buf), None);
2097 }
2098
2099 #[test]
2100 fn extract_http_host_incomplete_headers() {
2101 let buf = b"GET / HTTP/1.1\r\nHost: x";
2102 assert_eq!(extract_http_host(buf), None);
2103 }
2104
2105 #[test]
2106 fn extract_http_host_tls_first_byte() {
2107 let buf = [0x16u8, 0x03, 0x01, 0x00, 0x01];
2108 assert_eq!(extract_http_host(&buf), None);
2109 }
2110
2111 #[test]
2112 fn http_403_answers_only_confirmed_http1() {
2113 let get = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
2114 assert!(first_flight_is_http(get));
2115 assert!(!first_flight_is_http(b""));
2116 assert!(!first_flight_is_http(&[0x16, 0x03, 0x01]));
2117 assert!(!first_flight_is_http(b"\x00\x01binary"));
2118 }
2119
2120 #[test]
2121 fn extract_http_host_with_many_headers() {
2122 let mut req = Vec::from(&b"GET / HTTP/1.1\r\n"[..]);
2125 for i in 0..100 {
2126 req.extend_from_slice(format!("X-Pad-{i}: v\r\n").as_bytes());
2127 }
2128 req.extend_from_slice(b"Host: example.com\r\n\r\n");
2129 assert_eq!(extract_http_host(&req), Some("example.com".into()));
2130 }
2131
2132 use microsandbox_types::HttpDenyResponseFormat;
2135 use std::sync::Arc;
2136 use tokio::io::AsyncReadExt;
2137 use tokio::net::TcpListener;
2138 use tokio::task::JoinHandle;
2139
2140 use crate::secrets::config::{
2141 HostPattern, SecretEntry, SecretSubstitution, SecretViolationAction, SecretsConfig,
2142 };
2143
2144 fn make_plain_http_secret(placeholder: &str, value: &str, require_tls: bool) -> SecretsConfig {
2145 SecretsConfig {
2146 secrets: vec![SecretEntry {
2147 env_var: "API_KEY".into(),
2148 value: zeroize::Zeroizing::new(value.into()),
2149 source: None,
2150 placeholder: placeholder.into(),
2151 allowed_hosts: vec![HostPattern::Any],
2152 substitution: SecretSubstitution {
2153 headers: true,
2154 header_fields: Vec::new(),
2155 query: false,
2156 body: false,
2157 },
2158 passthrough_hosts: Vec::new(),
2159 violation_action: None,
2160 require_tls_identity: require_tls,
2161 }],
2162 ..Default::default()
2163 }
2164 }
2165
2166 fn make_host_bound_secret(placeholder: &str, value: &str, host: &str) -> SecretsConfig {
2167 SecretsConfig {
2168 secrets: vec![SecretEntry {
2169 env_var: "API_KEY".into(),
2170 value: zeroize::Zeroizing::new(value.into()),
2171 source: None,
2172 placeholder: placeholder.into(),
2173 allowed_hosts: vec![HostPattern::Exact(host.into())],
2174 substitution: SecretSubstitution::default(),
2175 passthrough_hosts: Vec::new(),
2176 violation_action: None,
2177 require_tls_identity: true,
2178 }],
2179 ..Default::default()
2180 }
2181 }
2182
2183 #[test]
2184 fn sanitize_connect_headers_blocks_placeholder_metadata_header_by_default() {
2185 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
2186 let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\nUser-Agent: curl\r\n\r\n";
2187
2188 assert_eq!(
2189 sanitize_connect_headers(headers, &secrets),
2190 Err(SecretViolationAction::BlockAndLog)
2191 );
2192 }
2193
2194 #[test]
2195 fn sanitize_connect_headers_respects_block_and_terminate() {
2196 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
2197 secrets.violation_action = SecretViolationAction::BlockAndTerminate;
2198 let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\n\r\n";
2199
2200 assert_eq!(
2201 sanitize_connect_headers(headers, &secrets),
2202 Err(SecretViolationAction::BlockAndTerminate)
2203 );
2204 }
2205
2206 #[test]
2207 fn sanitize_connect_headers_respects_explicit_passthrough() {
2208 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
2209 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
2210 let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\n\r\n";
2211
2212 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
2213
2214 assert_eq!(sanitized.as_ref(), headers);
2215 assert!(
2216 !String::from_utf8_lossy(sanitized.as_ref()).contains("real-secret-value"),
2217 "passthrough must never substitute real secrets into CONNECT metadata"
2218 );
2219 }
2220
2221 #[test]
2222 fn sanitize_connect_headers_keeps_safe_metadata_headers() {
2223 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
2224 let headers =
2225 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nUser-Agent: curl\r\n\r\n";
2226
2227 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
2228
2229 assert_eq!(sanitized.as_ref(), headers);
2230 }
2231
2232 #[test]
2233 fn sanitize_connect_headers_blocks_placeholder_in_request_line() {
2234 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
2235 let headers = b"CONNECT $MSB_KEY:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
2236
2237 assert_eq!(
2238 sanitize_connect_headers(headers, &secrets),
2239 Err(SecretViolationAction::BlockAndLog)
2240 );
2241 }
2242
2243 async fn spawn_sink() -> (SocketAddr, JoinHandle<Vec<u8>>) {
2244 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2245 let addr = listener.local_addr().unwrap();
2246 let handle = tokio::spawn(async move {
2247 let (mut stream, _) = listener.accept().await.unwrap();
2248 let mut received = Vec::new();
2249 let mut buf = vec![0u8; 4096];
2250 loop {
2251 match stream.read(&mut buf).await {
2252 Ok(0) | Err(_) => break,
2253 Ok(n) => received.extend_from_slice(&buf[..n]),
2254 }
2255 }
2256 received
2257 });
2258 (addr, handle)
2259 }
2260
2261 async fn assert_server_first_banner_is_immediate(
2262 policy: NetworkPolicy,
2263 tls_state: Option<Arc<TlsState>>,
2264 ) {
2265 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2266 let addr = listener.local_addr().unwrap();
2267 let server = tokio::spawn(async move {
2268 let (mut stream, _) = listener.accept().await.unwrap();
2269 stream.write_all(b"READY\n").await.unwrap();
2270 let mut received = Vec::new();
2271 stream.read_to_end(&mut received).await.unwrap();
2272 });
2273
2274 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2275 let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
2276 spawn_tcp_proxy(
2277 &tokio::runtime::Handle::current(),
2278 addr,
2279 addr,
2280 from_rx,
2281 to_tx,
2282 Arc::new(SharedState::new(4)),
2283 Arc::new(policy),
2284 Arc::new(SecretsConfig::default()),
2285 tls_state,
2286 false,
2287 Arc::new(ProxyConnectState::new()),
2288 None,
2289 );
2290
2291 let banner = tokio::time::timeout(Duration::from_secs(1), to_rx.recv())
2292 .await
2293 .expect("server-first banner was delayed by a pre-connect peek")
2294 .expect("proxy closed before relaying the server-first banner");
2295 assert_eq!(banner, b"READY\n"[..]);
2296
2297 drop(from_tx);
2298 tokio::time::timeout(Duration::from_secs(7), server)
2299 .await
2300 .expect("proxy did not close the upstream connection")
2301 .unwrap();
2302 }
2303
2304 #[tokio::test]
2305 async fn server_first_connection_skips_unrelated_domain_policy_peek() {
2306 let policy = NetworkPolicy {
2307 default_egress: Action::Allow,
2308 default_ingress: Action::Allow,
2309 rules: vec![allow_https("unused.example")],
2310 };
2311
2312 assert_server_first_banner_is_immediate(policy, None).await;
2313 }
2314
2315 #[tokio::test]
2316 async fn server_first_connection_skips_eager_connect_peek() {
2317 let _ = rustls::crypto::ring::default_provider().install_default();
2318 let tls_state = Arc::new(
2319 TlsState::new(
2320 microsandbox_types::TlsConfig::default(),
2321 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
2322 )
2323 .unwrap(),
2324 );
2325
2326 assert_server_first_banner_is_immediate(NetworkPolicy::default(), Some(tls_state)).await;
2327 }
2328
2329 async fn relay_through_proxy(
2330 request: Vec<u8>,
2331 secrets: SecretsConfig,
2332 handle: JoinHandle<Vec<u8>>,
2333 server_addr: SocketAddr,
2334 ) -> Vec<u8> {
2335 relay_through_proxy_with_policy(
2336 request,
2337 Arc::new(SharedState::new(4)),
2338 Arc::new(NetworkPolicy::default()),
2339 secrets,
2340 handle,
2341 server_addr,
2342 )
2343 .await
2344 }
2345
2346 async fn relay_through_proxy_with_policy(
2347 request: Vec<u8>,
2348 shared: Arc<SharedState>,
2349 policy: Arc<NetworkPolicy>,
2350 secrets: SecretsConfig,
2351 handle: JoinHandle<Vec<u8>>,
2352 server_addr: SocketAddr,
2353 ) -> Vec<u8> {
2354 relay_chunks_through_proxy_with_policy(
2355 vec![request],
2356 shared,
2357 policy,
2358 secrets,
2359 handle,
2360 server_addr,
2361 )
2362 .await
2363 }
2364
2365 async fn relay_chunks_through_proxy_with_policy(
2366 chunks: Vec<Vec<u8>>,
2367 shared: Arc<SharedState>,
2368 policy: Arc<NetworkPolicy>,
2369 secrets: SecretsConfig,
2370 handle: JoinHandle<Vec<u8>>,
2371 server_addr: SocketAddr,
2372 ) -> Vec<u8> {
2373 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2374 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2375 let secrets = Arc::new(secrets);
2376 let proxy_connect = Arc::new(ProxyConnectState::new());
2377
2378 for chunk in chunks {
2379 from_tx.send(Bytes::from(chunk)).await.unwrap();
2380 }
2381 drop(from_tx);
2382
2383 TcpProxy::new(
2384 None,
2385 server_addr,
2386 UpstreamTcpTarget::direct(server_addr),
2387 from_rx,
2388 to_tx,
2389 shared,
2390 policy,
2391 secrets,
2392 None,
2393 false,
2394 proxy_connect,
2395 None,
2396 )
2397 .try_run()
2398 .await
2399 .unwrap();
2400
2401 handle.await.unwrap()
2402 }
2403
2404 #[tokio::test]
2405 async fn plain_http_domain_policy_allows_matching_host() {
2406 let (addr, sink) = spawn_sink().await;
2407 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2408 let policy = Arc::new(NetworkPolicy {
2409 default_egress: Action::Deny,
2410 default_ingress: Action::Allow,
2411 rules: vec![allow_tcp("allowed.example", addr.port())],
2412 });
2413
2414 let wire = relay_through_proxy_with_policy(
2415 b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2416 shared,
2417 policy,
2418 SecretsConfig::default(),
2419 sink,
2420 addr,
2421 )
2422 .await;
2423
2424 assert_eq!(wire, b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2425 }
2426
2427 #[tokio::test]
2428 async fn plain_http_domain_policy_blocks_host_switch() {
2429 let (addr, sink) = spawn_sink().await;
2430 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2431 let policy = Arc::new(NetworkPolicy {
2432 default_egress: Action::Deny,
2433 default_ingress: Action::Allow,
2434 rules: vec![allow_tcp("allowed.example", addr.port())],
2435 });
2436
2437 let wire = relay_through_proxy_with_policy(
2438 b"GET / HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2439 shared,
2440 policy,
2441 SecretsConfig::default(),
2442 sink,
2443 addr,
2444 )
2445 .await;
2446
2447 assert!(
2448 wire.is_empty(),
2449 "switched HTTP authority must not reach upstream, got: {wire:?}"
2450 );
2451 }
2452
2453 #[tokio::test]
2454 async fn plain_http_domain_policy_blocks_keep_alive_host_switch() {
2455 let (addr, sink) = spawn_sink().await;
2456 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2457 let policy = Arc::new(NetworkPolicy {
2458 default_egress: Action::Deny,
2459 default_ingress: Action::Allow,
2460 rules: vec![allow_tcp("allowed.example", addr.port())],
2461 });
2462
2463 let wire = relay_chunks_through_proxy_with_policy(
2464 vec![
2465 b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2466 b"GET /two HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2467 ],
2468 shared,
2469 policy,
2470 SecretsConfig::default(),
2471 sink,
2472 addr,
2473 )
2474 .await;
2475
2476 assert_eq!(wire, b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2477 }
2478
2479 async fn relay_h2c_until_proxy_closes(chunks: Vec<Vec<u8>>) -> (Vec<u8>, bool) {
2489 let (addr, mut sink) = spawn_sink().await;
2490 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2491 let terminated = Arc::new(std::sync::atomic::AtomicBool::new(false));
2492 let flag = terminated.clone();
2493 shared.set_termination_hook(Arc::new(move || {
2494 flag.store(true, std::sync::atomic::Ordering::SeqCst);
2495 }));
2496 let policy = Arc::new(NetworkPolicy {
2497 default_egress: Action::Deny,
2498 default_ingress: Action::Allow,
2499 rules: vec![allow_tcp("allowed.example", addr.port())],
2500 });
2501 let (from_tx, from_rx) = mpsc::channel::<Bytes>(chunks.len());
2502 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2503 for chunk in chunks {
2504 from_tx.send(Bytes::from(chunk)).await.unwrap();
2505 }
2506
2507 let proxy = TcpProxy::new(
2508 None,
2509 addr,
2510 UpstreamTcpTarget::direct(addr),
2511 from_rx,
2512 to_tx,
2513 shared,
2514 policy,
2515 Arc::new(SecretsConfig::default()),
2516 None,
2517 false,
2518 Arc::new(ProxyConnectState::new()),
2519 None,
2520 )
2521 .try_run();
2522 tokio::time::timeout(Duration::from_secs(5), proxy)
2523 .await
2524 .expect("proxy kept the guest connection open")
2525 .unwrap();
2526 drop(from_tx);
2527
2528 let wire = match tokio::time::timeout(Duration::from_secs(5), &mut sink).await {
2529 Ok(wire) => wire.unwrap(),
2530 Err(_) => {
2531 sink.abort();
2532 panic!(
2533 "upstream sink never accepted a connection; the proxy refused the guest before dialing"
2534 );
2535 }
2536 };
2537 (wire, terminated.load(std::sync::atomic::Ordering::SeqCst))
2538 }
2539
2540 fn h2c_first_request(authority: &[u8]) -> Vec<u8> {
2544 let mut block = vec![0x82, 0x86, 0x84, 0x41, authority.len() as u8];
2545 block.extend_from_slice(authority);
2546 let mut first = H2_PREFACE.to_vec();
2547 first.extend(h2_frame(H2_SETTINGS, 0, 0, &[]));
2548 first.extend(h2_frame(
2549 H2_HEADERS,
2550 H2_END_STREAM | H2_END_HEADERS,
2551 1,
2552 &block,
2553 ));
2554 first
2555 }
2556
2557 fn h2c_first_request_wire(authority: &[u8]) -> Vec<u8> {
2560 let mut expected_block = Vec::new();
2561 for (name, value) in [
2562 (&b":method"[..], &b"GET"[..]),
2563 (b":scheme", b"http"),
2564 (b":path", b"/"),
2565 (b":authority", authority),
2566 ] {
2567 expected_block.extend_from_slice(&[0x10, name.len() as u8]);
2568 expected_block.extend_from_slice(name);
2569 expected_block.push(value.len() as u8);
2570 expected_block.extend_from_slice(value);
2571 }
2572 let mut expected = H2_PREFACE.to_vec();
2573 expected.extend(h2_frame(H2_SETTINGS, 0, 0, &[]));
2574 expected.extend(h2_frame(
2575 H2_HEADERS,
2576 H2_END_STREAM | H2_END_HEADERS,
2577 1,
2578 &expected_block,
2579 ));
2580 expected
2581 }
2582
2583 fn h2_frame(kind: u8, flags: u8, stream_id: u32, payload: &[u8]) -> Vec<u8> {
2585 let mut frame = (payload.len() as u32).to_be_bytes()[1..].to_vec();
2586 frame.extend_from_slice(&[kind, flags]);
2587 frame.extend_from_slice(&stream_id.to_be_bytes());
2588 frame.extend_from_slice(payload);
2589 frame
2590 }
2591
2592 const H2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
2593 const H2_HEADERS: u8 = 0x1;
2594 const H2_SETTINGS: u8 = 0x4;
2595 const H2_CONTINUATION: u8 = 0x9;
2596 const H2_END_STREAM: u8 = 0x1;
2597 const H2_END_HEADERS: u8 = 0x4;
2598
2599 #[tokio::test]
2600 async fn h2c_malformed_hpack_block_is_blocked_under_domain_policy() {
2601 let mut request = H2_PREFACE.to_vec();
2605 request.extend(h2_frame(
2606 H2_HEADERS,
2607 H2_END_STREAM | H2_END_HEADERS,
2608 1,
2609 &[0xff],
2610 ));
2611
2612 let (wire, terminated) = relay_h2c_until_proxy_closes(vec![request]).await;
2613
2614 assert!(
2616 wire.is_empty(),
2617 "malformed block reached upstream: {wire:02x?}"
2618 );
2619 assert!(!terminated);
2620 }
2621
2622 #[tokio::test]
2623 async fn h2c_late_hpack_error_closes_connection_after_valid_blocks() {
2624 let authority = b"allowed.example";
2629 let first = h2c_first_request(authority);
2630 let flags = H2_END_STREAM | H2_END_HEADERS;
2631 let second = h2_frame(H2_HEADERS, flags, 3, &[0x82, 0x86, 0x84, 0xbe, 0xff]);
2632 let third = h2_frame(H2_HEADERS, flags, 5, &[0x82, 0x86, 0x84, 0xbe]);
2633
2634 let (wire, terminated) = relay_h2c_until_proxy_closes(vec![first, second, third]).await;
2635
2636 assert_eq!(wire, h2c_first_request_wire(authority));
2639 assert!(!terminated);
2640 }
2641
2642 #[tokio::test]
2643 async fn h2c_decoder_error_after_partial_insertion_closes_connection() {
2644 let authority = b"allowed.example";
2655 let first = h2c_first_request(authority);
2656 let flags = H2_END_STREAM | H2_END_HEADERS;
2657 let failing = h2_frame(
2658 H2_HEADERS,
2659 flags,
2660 3,
2661 &[
2662 0x82, 0x86, 0x84, 0xbe, 0x40, 0x03, 0x78, 0x2d, 0x61, 0x01, 0x31, 0x80,
2663 ],
2664 );
2665 let third = h2_frame(H2_HEADERS, flags, 5, &[0x82, 0x86, 0x84, 0xbf]);
2666
2667 let (wire, terminated) = relay_h2c_until_proxy_closes(vec![first, failing, third]).await;
2668
2669 assert_eq!(wire, h2c_first_request_wire(authority));
2670 assert!(!terminated);
2671 }
2672
2673 #[tokio::test]
2674 async fn h2c_fragmented_malformed_hpack_block_is_blocked() {
2675 let settings = h2_frame(H2_SETTINGS, 0, 0, &[]);
2678 let headers = h2_frame(H2_HEADERS, H2_END_STREAM, 1, &[0x7f]);
2679 let continuation = h2_frame(H2_CONTINUATION, H2_END_HEADERS, 1, &[0xc5]);
2680 let (preface_head, preface_tail) = H2_PREFACE.split_at(18);
2681 let chunks = vec![
2682 preface_head.to_vec(),
2683 [preface_tail, &settings[..4]].concat(),
2684 [&settings[4..], &headers[..9]].concat(),
2685 headers[9..].to_vec(),
2686 continuation[..5].to_vec(),
2687 continuation[5..].to_vec(),
2688 ];
2689
2690 let (wire, terminated) = relay_h2c_until_proxy_closes(chunks).await;
2691
2692 assert_eq!(wire, [H2_PREFACE, &settings[..]].concat());
2693 assert!(!terminated);
2694 }
2695
2696 #[test]
2697 fn strict_hostname_allow_blocks_sni_authority_before_tcp_dial() {
2698 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2699 let shared = shared_with("allowed.example", "127.0.0.1");
2700 let policy = NetworkPolicy {
2701 default_egress: Action::Deny,
2702 default_ingress: Action::Allow,
2703 rules: vec![allow_tcp("allowed.example", dst.port())],
2704 };
2705
2706 assert!(strict_hostname_allow_is_opaque(
2707 true,
2708 &policy,
2709 dst,
2710 &shared,
2711 Some("allowed.example"),
2712 &synthetic_client_hello("allowed.example"),
2713 ));
2714 }
2715
2716 #[test]
2717 fn strict_hostname_allow_blocks_tls_without_sni_before_tcp_dial() {
2718 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2719 let shared = shared_with("allowed.example", "127.0.0.1");
2720 let policy = NetworkPolicy {
2721 default_egress: Action::Deny,
2722 default_ingress: Action::Allow,
2723 rules: vec![allow_tcp("allowed.example", dst.port())],
2724 };
2725
2726 assert!(strict_hostname_allow_is_opaque(
2727 true,
2728 &policy,
2729 dst,
2730 &shared,
2731 None,
2732 &[0x16, 0x03, 0x01],
2733 ));
2734 }
2735
2736 #[test]
2737 fn strict_hostname_allow_leaves_plain_http_for_authority_validation() {
2738 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 80);
2739 let shared = shared_with("allowed.example", "127.0.0.1");
2740 let policy = NetworkPolicy {
2741 default_egress: Action::Deny,
2742 default_ingress: Action::Allow,
2743 rules: vec![allow_tcp("allowed.example", dst.port())],
2744 };
2745
2746 assert!(!strict_hostname_allow_is_opaque(
2747 true,
2748 &policy,
2749 dst,
2750 &shared,
2751 None,
2752 b"GET / HTTP/1.1\r\n",
2753 ));
2754 }
2755
2756 #[tokio::test]
2757 async fn strict_mode_blocks_hostname_allowed_opaque_tls() {
2758 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2759 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2760 let policy = Arc::new(NetworkPolicy {
2761 default_egress: Action::Deny,
2762 default_ingress: Action::Allow,
2763 rules: vec![allow_tcp("allowed.example", dst.port())],
2764 });
2765 let proxy_connect = Arc::new(ProxyConnectState::new());
2766 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2767 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2768
2769 from_tx
2770 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2771 .await
2772 .unwrap();
2773 drop(from_tx);
2774
2775 TcpProxy::new(
2776 None,
2777 dst,
2778 UpstreamTcpTarget::direct(dst),
2779 from_rx,
2780 to_tx,
2781 shared,
2782 policy,
2783 Arc::new(SecretsConfig::default()),
2784 None,
2785 true,
2786 proxy_connect.clone(),
2787 None,
2788 )
2789 .try_run()
2790 .await
2791 .unwrap();
2792
2793 assert_eq!(proxy_connect.status(), ProxyConnectStatus::PolicyDenied);
2794 }
2795
2796 #[tokio::test]
2797 async fn strict_mode_leaves_default_allowed_opaque_tls_to_policy() {
2798 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 9);
2799 let shared = Arc::new(SharedState::new(4));
2800 let policy = Arc::new(NetworkPolicy {
2801 default_egress: Action::Allow,
2802 default_ingress: Action::Allow,
2803 rules: vec![Rule::deny_egress(Destination::Domain(
2804 "blocked.example".parse().unwrap(),
2805 ))],
2806 });
2807 let proxy_connect = Arc::new(ProxyConnectState::new());
2808 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2809 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2810
2811 from_tx
2812 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2813 .await
2814 .unwrap();
2815 drop(from_tx);
2816
2817 let result = TcpProxy::new(
2818 None,
2819 dst,
2820 UpstreamTcpTarget::direct(dst),
2821 from_rx,
2822 to_tx,
2823 shared,
2824 policy,
2825 Arc::new(SecretsConfig::default()),
2826 None,
2827 true,
2828 proxy_connect.clone(),
2829 None,
2830 )
2831 .try_run()
2832 .await;
2833
2834 assert!(result.is_err(), "dummy upstream should refuse the dial");
2835 assert_eq!(
2836 proxy_connect.status(),
2837 ProxyConnectStatus::UpstreamConnectFailed
2838 );
2839 }
2840
2841 #[tokio::test]
2842 async fn server_first_http_like_binary_first_flight_is_forwarded() {
2843 use tokio::net::TcpListener;
2844
2845 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2846 let addr = listener.local_addr().unwrap();
2847 let server = tokio::spawn(async move {
2848 let (mut stream, _) = listener.accept().await.unwrap();
2849 stream
2850 .write_all(b"binary server-first greeting")
2851 .await
2852 .unwrap();
2853 stream.flush().await.unwrap();
2854
2855 let mut received = Vec::new();
2856 stream.read_to_end(&mut received).await.unwrap();
2857 received
2858 });
2859
2860 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2861 let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
2862 spawn_tcp_proxy(
2863 &tokio::runtime::Handle::current(),
2864 addr,
2865 addr,
2866 from_rx,
2867 to_tx,
2868 Arc::new(SharedState::new(4)),
2869 Arc::new(NetworkPolicy::default()),
2870 Arc::new(make_plain_http_secret(
2871 "$MSB_UNUSED",
2872 "unused-secret-value",
2873 false,
2874 )),
2875 None,
2876 false,
2877 Arc::new(ProxyConnectState::new()),
2878 None,
2879 );
2880
2881 let greeting = to_rx.recv().await.unwrap();
2882 assert_eq!(greeting, b"binary server-first greeting"[..]);
2883
2884 let first_flight = Bytes::from_static(b"BINARY3 v1\x00\x01opaque request");
2887 from_tx.send(first_flight.clone()).await.unwrap();
2888 drop(from_tx);
2889
2890 let wire = tokio::time::timeout(Duration::from_secs(2), server)
2891 .await
2892 .expect("proxy did not finish forwarding the client first flight")
2893 .unwrap();
2894 assert_eq!(wire, first_flight);
2895 }
2896
2897 #[tokio::test]
2898 async fn plain_http_substitutes_placeholder_when_host_arrives_in_second_segment() {
2899 let (addr, sink) = spawn_sink().await;
2902 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2903
2904 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2905 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2906 let proxy_connect = Arc::new(ProxyConnectState::new());
2907
2908 from_tx
2909 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2910 .await
2911 .unwrap();
2912 from_tx
2913 .send(Bytes::from_static(
2914 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2915 ))
2916 .await
2917 .unwrap();
2918 drop(from_tx);
2919
2920 TcpProxy::new(
2921 None,
2922 addr,
2923 UpstreamTcpTarget::direct(addr),
2924 from_rx,
2925 to_tx,
2926 Arc::new(SharedState::new(4)),
2927 Arc::new(NetworkPolicy::default()),
2928 Arc::new(secrets),
2929 None,
2930 false,
2931 proxy_connect,
2932 None,
2933 )
2934 .try_run()
2935 .await
2936 .unwrap();
2937
2938 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2939 assert!(wire.contains("real-secret-value"), "got: {wire:?}");
2940 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2941 }
2942
2943 #[tokio::test]
2944 async fn plain_http_passthrough_handles_a_host_in_split_headers() {
2945 let (addr, sink) = spawn_sink().await;
2949
2950 let shared = SharedState::new(4);
2951 shared.cache_resolved_hostname(
2952 "example.com",
2953 ResolvedHostnameFamily::Ipv4,
2954 ["127.0.0.1".parse::<IpAddr>().unwrap()],
2955 StdDuration::from_secs(60),
2956 );
2957
2958 let secrets = SecretsConfig {
2959 secrets: vec![SecretEntry {
2960 env_var: "API_KEY".into(),
2961 value: zeroize::Zeroizing::new("real-secret-value".into()),
2962 source: None,
2963 placeholder: "$MSB_KEY".into(),
2964 allowed_hosts: vec![HostPattern::Exact("example.com".into())],
2965 substitution: SecretSubstitution {
2966 headers: true,
2967 header_fields: Vec::new(),
2968 query: false,
2969 body: false,
2970 },
2971 passthrough_hosts: vec![HostPattern::Exact("example.com".into())],
2972 violation_action: None,
2973 require_tls_identity: true,
2974 }],
2975 ..Default::default()
2976 };
2977
2978 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2979 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2980 let proxy_connect = Arc::new(ProxyConnectState::new());
2981
2982 from_tx
2983 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2984 .await
2985 .unwrap();
2986 from_tx
2987 .send(Bytes::from_static(
2988 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2989 ))
2990 .await
2991 .unwrap();
2992 drop(from_tx);
2993
2994 TcpProxy::new(
2995 None,
2996 addr,
2997 UpstreamTcpTarget::direct(addr),
2998 from_rx,
2999 to_tx,
3000 Arc::new(shared),
3001 Arc::new(NetworkPolicy::default()),
3002 Arc::new(secrets),
3003 None,
3004 false,
3005 proxy_connect,
3006 None,
3007 )
3008 .try_run()
3009 .await
3010 .unwrap();
3011
3012 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
3013 assert!(
3014 wire.contains("Host: example.com"),
3015 "request must reach the allowed host, got: {wire:?}"
3016 );
3017 assert!(
3018 wire.contains("$MSB_KEY"),
3019 "placeholder must be forwarded unchanged for a require_tls_identity secret, got: {wire:?}"
3020 );
3021 assert!(
3022 !wire.contains("real-secret-value"),
3023 "secret must never be substituted over plain HTTP, got: {wire:?}"
3024 );
3025 }
3026
3027 #[tokio::test]
3028 async fn plain_http_substitutes_placeholder_in_first_flight() {
3029 let (addr, sink) = spawn_sink().await;
3030
3031 let request =
3032 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
3033 .to_vec();
3034 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
3035
3036 let wire =
3037 String::from_utf8(relay_through_proxy(request, secrets, sink, addr).await).unwrap();
3038 assert!(
3039 wire.contains("real-secret-value"),
3040 "real value must reach server, got: {wire:?}"
3041 );
3042 assert!(
3043 !wire.contains("$MSB_KEY"),
3044 "placeholder must not reach server, got: {wire:?}"
3045 );
3046 }
3047
3048 #[tokio::test]
3049 async fn plain_http_no_substitution_when_require_tls_identity_true() {
3050 let (addr, sink) = spawn_sink().await;
3051
3052 let request =
3053 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
3054 .to_vec();
3055 let mut secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", true);
3056 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
3057
3058 let wire =
3059 String::from_utf8_lossy(&relay_through_proxy(request, secrets, sink, addr).await)
3060 .into_owned();
3061 assert!(
3062 wire.contains("$MSB_KEY"),
3063 "placeholder must be forwarded unchanged when require_tls_identity=true, got: {wire:?}"
3064 );
3065 assert!(
3066 !wire.contains("real-secret-value"),
3067 "real value must not leak when require_tls_identity=true, got: {wire:?}"
3068 );
3069 }
3070
3071 #[tokio::test]
3072 async fn plain_http_large_body_forwarded_verbatim_in_relay_loop() {
3073 let (addr, sink) = spawn_sink().await;
3077 let secrets = make_plain_http_secret("$MSB_KEY", "real-value", false);
3078
3079 let body = "x".repeat(32_000);
3080 let header = format!(
3081 "POST /upload HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\nContent-Length: {}\r\n\r\n",
3082 body.len()
3083 );
3084
3085 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
3086 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
3087 let proxy_connect = Arc::new(ProxyConnectState::new());
3088
3089 from_tx
3090 .send(Bytes::from(header.into_bytes()))
3091 .await
3092 .unwrap();
3093 from_tx
3094 .send(Bytes::from(body.clone().into_bytes()))
3095 .await
3096 .unwrap();
3097 drop(from_tx);
3098
3099 TcpProxy::new(
3100 None,
3101 addr,
3102 UpstreamTcpTarget::direct(addr),
3103 from_rx,
3104 to_tx,
3105 Arc::new(SharedState::new(4)),
3106 Arc::new(NetworkPolicy::default()),
3107 Arc::new(secrets),
3108 None,
3109 false,
3110 proxy_connect,
3111 None,
3112 )
3113 .try_run()
3114 .await
3115 .unwrap();
3116
3117 let wire = String::from_utf8_lossy(&sink.await.unwrap()).into_owned();
3118 assert!(wire.contains(&body), "got {} bytes", wire.len());
3119 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
3120 }
3121}