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::secrets::config::SecretsConfigExt;
24use crate::engine::tls::proxy::TlsProxy;
25use crate::engine::tls::sni;
26use crate::engine::tls::state::TlsState;
27use crate::netstack::shared::SharedState;
28use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
29use crate::proxy::ResolvedOutboundProxy;
30use crate::secrets::config::{SecretViolationAction, SecretsConfig};
31use crate::secrets::handler::{
32 SecretsHandler, first_line_is_not_http_request, looks_like_http_request_prefix,
33};
34
35const SERVER_READ_BUF_SIZE: usize = 16384;
41
42const CONNECT_RESP_LIMIT: usize = 8192;
44
45const PEEK_BUF_SIZE: usize = 16384;
47
48const PEEK_BUDGET: Duration = Duration::from_secs(5);
51
52#[derive(Debug)]
57struct ConnectRequest {
58 bytes: Vec<u8>,
59 header_end: usize,
60 target: ConnectTarget,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64struct ConnectTarget {
65 host: String,
66 port: u16,
67 expected_sni: Option<String>,
68}
69
70pub(crate) struct TcpProxy {
72 guest_dst: SocketAddr,
73 connect_target: UpstreamTcpTarget,
74 from_smoltcp: mpsc::Receiver<Bytes>,
75 to_smoltcp: mpsc::Sender<Bytes>,
76 shared: Arc<SharedState>,
77 network_policy: Arc<NetworkPolicy>,
78 secrets: Arc<SecretsConfig>,
79 tls_state: Option<Arc<TlsState>>,
80 strict: bool,
81 proxy_connect: Arc<ProxyConnectState>,
82 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
83}
84
85impl ConnectRequest {
90 fn header_bytes(&self) -> &[u8] {
91 &self.bytes[..self.header_end]
92 }
93
94 fn post_header_bytes(&self) -> &[u8] {
95 &self.bytes[self.header_end..]
96 }
97}
98
99impl ConnectTarget {
100 fn is_intercepted(&self, tls_state: &TlsState) -> bool {
101 tls_state.config.intercepted_ports.contains(&self.port)
102 }
103
104 fn guest_dst(&self, fallback: SocketAddr, shared: &SharedState) -> SocketAddr {
105 if let Ok(ip) = self.host.parse::<IpAddr>() {
106 return SocketAddr::new(ip, self.port);
107 }
108
109 if self.host.eq_ignore_ascii_case(crate::HOST_ALIAS) {
110 match fallback.ip() {
111 IpAddr::V4(_) => {
112 if let Some(ip) = shared.gateway_ipv4() {
113 return SocketAddr::new(IpAddr::V4(ip), self.port);
114 }
115 }
116 IpAddr::V6(_) => {
117 if let Some(ip) = shared.gateway_ipv6() {
118 return SocketAddr::new(IpAddr::V6(ip), self.port);
119 }
120 }
121 }
122 if let Some(ip) = shared.gateway_ipv4() {
123 return SocketAddr::new(IpAddr::V4(ip), self.port);
124 }
125 if let Some(ip) = shared.gateway_ipv6() {
126 return SocketAddr::new(IpAddr::V6(ip), self.port);
127 }
128 }
129
130 SocketAddr::new(fallback.ip(), self.port)
131 }
132}
133
134impl TcpProxy {
135 #[allow(clippy::too_many_arguments)]
137 pub(crate) fn new(
138 guest_dst: SocketAddr,
139 connect_target: UpstreamTcpTarget,
140 from_smoltcp: mpsc::Receiver<Bytes>,
141 to_smoltcp: mpsc::Sender<Bytes>,
142 shared: Arc<SharedState>,
143 network_policy: Arc<NetworkPolicy>,
144 secrets: Arc<SecretsConfig>,
145 tls_state: Option<Arc<TlsState>>,
146 strict: bool,
147 proxy_connect: Arc<ProxyConnectState>,
148 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
149 ) -> Self {
150 Self {
151 guest_dst,
152 connect_target,
153 from_smoltcp,
154 to_smoltcp,
155 shared,
156 network_policy,
157 secrets,
158 tls_state,
159 strict,
160 proxy_connect,
161 outbound_proxy,
162 }
163 }
164
165 pub(crate) async fn run(self) {
167 let guest_dst = self.guest_dst;
168 let connect_dst = self.connect_target.primary();
169
170 if let Err(error) = self.try_run().await {
171 tracing::debug!(
172 dst = %connect_dst,
173 %guest_dst,
174 %error,
175 "TCP proxy task ended",
176 );
177 }
178 }
179
180 async fn try_run(self) -> io::Result<()> {
182 let Self {
183 guest_dst,
184 connect_target,
185 mut from_smoltcp,
186 to_smoltcp,
187 shared,
188 network_policy,
189 secrets,
190 tls_state,
191 strict,
192 proxy_connect,
193 outbound_proxy,
194 } = self;
195
196 let (mut initial_buf, sni) = if network_policy.has_domain_rules() {
202 peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await
203 } else {
204 (Vec::new(), None)
205 };
206
207 if network_policy.has_domain_rules() {
213 let source = match sni.as_deref() {
214 Some(name) => HostnameSource::Sni(name),
215 None => HostnameSource::CacheOnly,
216 };
217 match network_policy.evaluate_egress_with_source(
218 guest_dst,
219 Protocol::Tcp,
220 &shared,
221 source,
222 ) {
223 EgressEvaluation::Allow => {
224 if strict_hostname_allow_is_opaque(
225 strict,
226 &network_policy,
227 guest_dst,
228 &shared,
229 sni.as_deref(),
230 &initial_buf,
231 ) {
232 tracing::debug!(
233 sni = sni.as_deref(),
234 dst = %guest_dst,
235 "TCP egress denied by strict hostname policy",
236 );
237 proxy_connect.mark_policy_denied();
238 shared.proxy_wake.wake();
239 return Ok(());
240 }
241 }
242 EgressEvaluation::Deny => {
243 tracing::debug!(
244 dst = %guest_dst,
245 source = source.label(),
246 "TCP egress denied by domain policy",
247 );
248 proxy_connect.mark_policy_denied();
249 shared.proxy_wake.wake();
250 return Ok(());
251 }
252 EgressEvaluation::DeferUntilHostname => {
253 debug_assert!(false, "DeferUntilHostname leaked into TCP proxy task");
254 proxy_connect.mark_policy_denied();
255 shared.proxy_wake.wake();
256 return Ok(());
257 }
258 }
259 }
260
261 if let Some(tls_state) = tls_state.clone() {
263 if initial_buf.is_empty() {
264 let (peeked, _) = peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await;
265 initial_buf = peeked;
266 }
267 if could_be_connect_request(&initial_buf) {
268 return handle_connect_tunnel(
269 guest_dst,
270 connect_target,
271 initial_buf,
272 from_smoltcp,
273 to_smoltcp,
274 shared,
275 network_policy,
276 tls_state,
277 strict,
278 proxy_connect,
279 outbound_proxy,
280 None,
281 )
282 .await;
283 }
284 }
285
286 let stream = connect_target
291 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
292 .await?;
293 let connect_dst = stream.peer_addr().unwrap_or(connect_target.primary());
294 let (mut server_rx, mut server_tx) = stream.into_split();
295
296 let enforce_http_authority = network_policy.has_domain_rules();
302 let want_headers = enforce_http_authority
303 || secrets.has_plain_http_candidates()
304 || secrets.has_host_scoped_secrets();
305 let (initial_buf, is_tls) = if want_headers {
306 classify_first_flight(
307 initial_buf,
308 &mut from_smoltcp,
309 &mut server_rx,
310 &to_smoltcp,
311 &shared,
312 want_headers,
313 PEEK_BUF_SIZE,
314 PEEK_BUDGET,
315 )
316 .await?
317 } else {
318 (initial_buf, false)
319 };
320
321 if let Some(tls_state) = tls_state.clone()
322 && could_be_connect_request(&initial_buf)
323 {
324 let proxy_stream = server_rx
329 .reunite(server_tx)
330 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
331 return handle_connect_tunnel(
332 guest_dst,
333 connect_target,
334 initial_buf,
335 from_smoltcp,
336 to_smoltcp,
337 shared,
338 network_policy,
339 tls_state,
340 strict,
341 proxy_connect,
342 outbound_proxy,
343 Some(proxy_stream),
344 )
345 .await;
346 }
347
348 let mut late_connect_state = tls_state;
349 let mut secrets_handler: Option<SecretsHandler> = if is_tls {
350 None
351 } else if enforce_http_authority {
352 let host = extract_http_host(&initial_buf).unwrap_or_default();
353 Some(SecretsHandler::new_plain_http_policy(
354 &secrets,
355 &host,
356 guest_dst,
357 network_policy.clone(),
358 shared.clone(),
359 ))
360 } else if !secrets.secrets.is_empty() {
361 Some(match extract_http_host(&initial_buf) {
362 Some(host) => {
363 SecretsHandler::new_plain_http(&secrets, &host, guest_dst.ip(), &shared)
364 }
365 None => SecretsHandler::new_plain_http_invalid_host(&secrets),
366 })
367 } else {
368 None
369 };
370
371 if !initial_buf.is_empty() {
373 let out: Cow<[u8]> = match secrets_handler.as_mut() {
374 Some(h) => match h.substitute(&initial_buf) {
375 Ok(cow) => cow,
378 Err(action) => {
379 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation in first flight");
380 if matches!(action, SecretViolationAction::BlockAndTerminate) {
381 shared.trigger_termination();
382 }
383 return Ok(());
384 }
385 },
386 None => Cow::Borrowed(&initial_buf),
387 };
388 if !out.is_empty() {
389 if let Err(e) = server_tx.write_all(&out).await {
390 tracing::debug!(dst = %connect_dst, error = %e, "replay of buffered first flight failed");
391 return Ok(());
392 }
393 if let Err(e) = server_tx.flush().await {
394 tracing::debug!(dst = %connect_dst, error = %e, "flush after first flight failed");
395 return Ok(());
396 }
397 }
398 }
399
400 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
401
402 let mut guest_eof = false;
407 loop {
408 tokio::select! {
409 data = from_smoltcp.recv(), if !guest_eof => {
411 match data {
412 Some(bytes) => {
413 if let Some(tls_state) = late_connect_state.take()
414 && could_be_connect_request(&bytes)
415 {
416 let proxy_stream = server_rx
421 .reunite(server_tx)
422 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
423 return handle_connect_tunnel(
424 guest_dst,
425 connect_target,
426 bytes.to_vec(),
427 from_smoltcp,
428 to_smoltcp,
429 shared,
430 network_policy,
431 tls_state,
432 strict,
433 proxy_connect,
434 outbound_proxy,
435 Some(proxy_stream),
436 )
437 .await;
438 }
439 let out: Cow<[u8]> = match secrets_handler.as_mut() {
442 Some(h) => match h.substitute(&bytes) {
443 Ok(cow) => cow,
444 Err(action) => {
445 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation");
446 if matches!(action, SecretViolationAction::BlockAndTerminate)
447 {
448 shared.trigger_termination();
449 }
450 break;
451 }
452 },
453 None => Cow::Borrowed(&bytes),
454 };
455 if !out.is_empty() {
456 if let Err(e) = server_tx.write_all(&out).await {
457 tracing::debug!(dst = %connect_dst, error = %e, "write to server failed");
458 break;
459 }
460 if let Err(e) = server_tx.flush().await {
461 tracing::debug!(dst = %connect_dst, error = %e, "flush to server failed");
462 break;
463 }
464 }
465 }
466 None => {
471 guest_eof = true;
472 if server_tx.shutdown().await.is_err() {
473 break;
474 }
475 }
476 }
477 }
478
479 result = server_rx.read(&mut server_buf) => {
481 match result {
482 Ok(0) => break, Ok(n) => {
484 late_connect_state = None;
487 let data = Bytes::copy_from_slice(&server_buf[..n]);
488 if to_smoltcp.send(data).await.is_err() {
489 break;
491 }
492 shared.proxy_wake.wake();
495 }
496 Err(e) => {
497 tracing::debug!(dst = %connect_dst, error = %e, "read from server failed");
498 break;
499 }
500 }
501 }
502 }
503 }
504
505 Ok(())
506 }
507}
508
509#[allow(clippy::too_many_arguments)]
522pub fn spawn_tcp_proxy(
523 handle: &tokio::runtime::Handle,
524 guest_dst: SocketAddr,
525 connect_dst: SocketAddr,
526 from_smoltcp: mpsc::Receiver<Bytes>,
527 to_smoltcp: mpsc::Sender<Bytes>,
528 shared: Arc<SharedState>,
529 network_policy: Arc<NetworkPolicy>,
530 secrets: Arc<SecretsConfig>,
531 tls_state: Option<Arc<TlsState>>,
532 strict: bool,
533 proxy_connect: Arc<ProxyConnectState>,
534 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
535) {
536 let proxy = TcpProxy::new(
537 guest_dst,
538 UpstreamTcpTarget::direct(connect_dst),
539 from_smoltcp,
540 to_smoltcp,
541 shared,
542 network_policy,
543 secrets,
544 tls_state,
545 strict,
546 proxy_connect,
547 outbound_proxy,
548 );
549
550 handle.spawn(proxy.run());
551}
552
553fn strict_hostname_allow_is_opaque(
554 strict: bool,
555 network_policy: &NetworkPolicy,
556 guest_dst: SocketAddr,
557 shared: &SharedState,
558 sni: Option<&str>,
559 initial_buf: &[u8],
560) -> bool {
561 if !strict {
562 return false;
563 }
564
565 let source = if let Some(name) = sni {
566 HostnameSource::Sni(name)
567 } else if initial_buf.is_empty() || initial_buf.first() == Some(&0x16) {
568 HostnameSource::CacheOnly
569 } else {
570 return false;
571 };
572
573 network_policy.allows_egress_via_hostname(guest_dst, Protocol::Tcp, shared, source)
574}
575
576#[allow(clippy::too_many_arguments)]
582async fn handle_connect_tunnel(
583 guest_dst: SocketAddr,
584 proxy_target: UpstreamTcpTarget,
585 initial_buf: Vec<u8>,
586 mut from_smoltcp: mpsc::Receiver<Bytes>,
587 to_smoltcp: mpsc::Sender<Bytes>,
588 shared: Arc<SharedState>,
589 network_policy: Arc<NetworkPolicy>,
590 tls_state: Arc<TlsState>,
591 strict: bool,
592 proxy_connect: Arc<ProxyConnectState>,
593 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
594 preconnected_proxy: Option<TcpStream>,
595) -> io::Result<()> {
596 let proxy_dst = proxy_target.primary();
597 let connect_req =
598 parse_connect_request(buffer_connect_request(initial_buf, &mut from_smoltcp).await?)?;
599
600 let connect_headers = match sanitize_connect_headers(
601 connect_req.header_bytes(),
602 &tls_state.secrets.load(),
603 ) {
604 Ok(headers) => headers,
605 Err(action) => {
606 tracing::warn!(dst = %proxy_dst, violation = ?action, "secret violation in CONNECT headers");
607 if matches!(action, SecretViolationAction::BlockAndTerminate) {
608 shared.trigger_termination();
609 }
610 return Ok(());
611 }
612 };
613
614 let mut proxy_stream = match preconnected_proxy {
616 Some(stream) => stream,
617 None => {
618 proxy_target
619 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
620 .await?
621 }
622 };
623
624 if !connect_req.target.is_intercepted(&tls_state) {
625 let tunnel_dst = connect_req.target.guest_dst(guest_dst, &shared);
626 if strict
627 && let Some(expected_sni) = connect_req.target.expected_sni.as_deref()
628 && network_policy.allows_egress_via_hostname(
629 tunnel_dst,
630 Protocol::Tcp,
631 &shared,
632 HostnameSource::Sni(expected_sni),
633 )
634 {
635 tracing::debug!(
636 sni = %expected_sni,
637 dst = %tunnel_dst,
638 "CONNECT tunnel denied by strict hostname policy",
639 );
640 proxy_connect.mark_policy_denied();
641 shared.proxy_wake.wake();
642 return Ok(());
643 }
644 proxy_stream.write_all(&connect_headers).await?;
645 proxy_stream.flush().await?;
646 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
647 if to_smoltcp
648 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
649 .await
650 .is_err()
651 {
652 return Ok(());
653 }
654 if !proxy_resp[header_end..].is_empty()
655 && to_smoltcp
656 .send(Bytes::copy_from_slice(&proxy_resp[header_end..]))
657 .await
658 .is_err()
659 {
660 return Ok(());
661 }
662 shared.proxy_wake.wake();
663 if !connect_response_is_success(&proxy_resp[..header_end]) {
664 proxy_connect.mark_connected();
665 return Ok(());
666 }
667 if !connect_req.post_header_bytes().is_empty() {
668 proxy_stream
669 .write_all(connect_req.post_header_bytes())
670 .await?;
671 }
672 proxy_stream.flush().await?;
673 proxy_connect.mark_connected();
674 return relay_connected_stream(proxy_stream, from_smoltcp, to_smoltcp, shared).await;
675 }
676
677 proxy_stream.write_all(&connect_headers).await?;
678 proxy_stream.flush().await?;
679
680 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
681 if !connect_response_is_success(&proxy_resp[..header_end]) {
682 return Err(io::Error::new(
683 io::ErrorKind::ConnectionRefused,
684 "proxy rejected CONNECT",
685 ));
686 }
687 if !proxy_resp[header_end..].is_empty() {
688 return Err(io::Error::new(
689 io::ErrorKind::InvalidData,
690 "proxy sent unexpected bytes after CONNECT response headers",
691 ));
692 }
693 proxy_connect.mark_connected();
694
695 if to_smoltcp
696 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
697 .await
698 .is_err()
699 {
700 return Ok(());
701 }
702 shared.proxy_wake.wake();
703
704 let tls_seed = connect_req.post_header_bytes().to_vec();
705 let tls_guest_dst = connect_req.target.guest_dst(guest_dst, &shared);
706 let expected_sni = connect_req.target.expected_sni.clone();
707
708 TlsProxy::new(
709 tls_guest_dst,
710 proxy_target,
711 from_smoltcp,
712 to_smoltcp,
713 shared,
714 tls_state,
715 network_policy,
716 strict,
717 proxy_connect,
718 None,
722 )
723 .with_upstream(proxy_stream)
724 .with_expected_sni(expected_sni)
725 .with_initial_buf(tls_seed)
726 .try_run()
727 .await
728}
729
730async fn relay_connected_stream(
732 stream: TcpStream,
733 mut from_smoltcp: mpsc::Receiver<Bytes>,
734 to_smoltcp: mpsc::Sender<Bytes>,
735 shared: Arc<SharedState>,
736) -> io::Result<()> {
737 let (mut server_rx, mut server_tx) = stream.into_split();
738 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
739
740 let mut guest_eof = false;
741 loop {
742 tokio::select! {
743 data = from_smoltcp.recv(), if !guest_eof => {
744 match data {
745 Some(bytes) => {
746 server_tx.write_all(&bytes).await?;
747 server_tx.flush().await?;
748 }
749 None => {
752 guest_eof = true;
753 if server_tx.shutdown().await.is_err() {
754 break;
755 }
756 }
757 }
758 }
759 result = server_rx.read(&mut server_buf) => {
760 match result {
761 Ok(0) => break,
762 Ok(n) => {
763 if to_smoltcp
764 .send(Bytes::copy_from_slice(&server_buf[..n]))
765 .await
766 .is_err()
767 {
768 break;
769 }
770 shared.proxy_wake.wake();
771 }
772 Err(e) => return Err(e),
773 }
774 }
775 }
776 }
777
778 Ok(())
779}
780
781async fn buffer_connect_request(
782 mut buf: Vec<u8>,
783 from_smoltcp: &mut mpsc::Receiver<Bytes>,
784) -> io::Result<Vec<u8>> {
785 let timeout_fut = tokio::time::sleep(PEEK_BUDGET);
786 tokio::pin!(timeout_fut);
787
788 loop {
789 if !could_be_connect_request(&buf) {
790 return Err(io::Error::new(
791 io::ErrorKind::InvalidData,
792 "malformed CONNECT request prefix",
793 ));
794 }
795 if headers_end(&buf).is_some() {
796 return Ok(buf);
797 }
798 if buf.len() >= PEEK_BUF_SIZE {
799 return Err(io::Error::new(
800 io::ErrorKind::InvalidData,
801 "CONNECT request headers too large",
802 ));
803 }
804
805 tokio::select! {
806 biased;
807 _ = &mut timeout_fut => {
808 return Err(io::Error::new(
809 io::ErrorKind::TimedOut,
810 "timed out waiting for complete CONNECT request headers",
811 ));
812 }
813 data = from_smoltcp.recv() => match data {
814 Some(bytes) => {
815 buf.extend_from_slice(&bytes);
816 }
817 None => {
818 return Err(io::Error::new(
819 io::ErrorKind::UnexpectedEof,
820 "channel closed before complete CONNECT request headers",
821 ));
822 }
823 }
824 }
825 }
826}
827
828async fn read_connect_response_headers(stream: &mut TcpStream) -> io::Result<(Vec<u8>, usize)> {
829 tokio::time::timeout(PEEK_BUDGET, async {
830 let mut proxy_resp = Vec::with_capacity(256);
831 let mut buf = [0u8; 4096];
832 loop {
833 let n = stream.read(&mut buf).await?;
834 if n == 0 {
835 return Err(io::Error::new(
836 io::ErrorKind::UnexpectedEof,
837 "proxy closed before sending CONNECT response",
838 ));
839 }
840 proxy_resp.extend_from_slice(&buf[..n]);
841 if let Some(end) = headers_end(&proxy_resp) {
842 return Ok((proxy_resp, end));
843 }
844 if proxy_resp.len() > CONNECT_RESP_LIMIT {
845 return Err(io::Error::new(
846 io::ErrorKind::InvalidData,
847 "proxy CONNECT response too large",
848 ));
849 }
850 }
851 })
852 .await
853 .map_err(|_| {
854 io::Error::new(
855 io::ErrorKind::TimedOut,
856 "timed out waiting for proxy CONNECT response",
857 )
858 })?
859}
860
861fn sanitize_connect_headers<'a>(
862 header_bytes: &'a [u8],
863 secrets: &SecretsConfig,
864) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
865 if secrets.secrets.is_empty() {
866 return Ok(Cow::Borrowed(header_bytes));
867 }
868
869 let mut handler = SecretsHandler::new_plain_http_untrusted_metadata(secrets);
870 handler.substitute(header_bytes)
871}
872
873fn headers_end(buf: &[u8]) -> Option<usize> {
875 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
876}
877
878fn could_be_connect_request(buf: &[u8]) -> bool {
879 const PREFIX: &[u8] = b"CONNECT ";
880 if buf.is_empty() {
881 return false;
882 }
883 let n = buf.len().min(PREFIX.len());
884 buf[..n].eq_ignore_ascii_case(&PREFIX[..n])
885}
886
887fn parse_connect_request(bytes: Vec<u8>) -> io::Result<ConnectRequest> {
888 let header_end = headers_end(&bytes).ok_or_else(|| {
889 io::Error::new(
890 io::ErrorKind::InvalidData,
891 "incomplete CONNECT request headers",
892 )
893 })?;
894 let target = {
895 let request_line = bytes[..header_end]
896 .split(|&b| b == b'\n')
897 .next()
898 .unwrap_or(&[]);
899 let request_line = std::str::from_utf8(request_line)
900 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "CONNECT line is not UTF-8"))?
901 .trim_end_matches('\r');
902 let mut parts = request_line.split_ascii_whitespace();
903 let method = parts.next().unwrap_or_default();
904 let authority = parts.next().unwrap_or_default();
905 let version = parts.next().unwrap_or_default();
906 if !method.eq_ignore_ascii_case("CONNECT")
907 || authority.is_empty()
908 || !is_http_version(version)
909 || parts.next().is_some()
910 {
911 return Err(io::Error::new(
912 io::ErrorKind::InvalidData,
913 "malformed CONNECT request line",
914 ));
915 }
916 parse_connect_target(authority)?
917 };
918
919 Ok(ConnectRequest {
920 bytes,
921 header_end,
922 target,
923 })
924}
925
926fn parse_connect_target(authority: &str) -> io::Result<ConnectTarget> {
927 let authority = authority.trim();
928 let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
929 let (host, rest) = rest.split_once(']').ok_or_else(|| {
930 io::Error::new(
931 io::ErrorKind::InvalidData,
932 "malformed CONNECT IPv6 authority",
933 )
934 })?;
935 let port = rest.strip_prefix(':').ok_or_else(|| {
936 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
937 })?;
938 (host, port)
939 } else {
940 let (host, port) = authority.rsplit_once(':').ok_or_else(|| {
941 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
942 })?;
943 if host.contains(':') {
944 return Err(io::Error::new(
945 io::ErrorKind::InvalidData,
946 "CONNECT IPv6 authority must be bracketed",
947 ));
948 }
949 (host, port)
950 };
951 let host = host.trim().trim_end_matches('.');
952 if host.is_empty() {
953 return Err(io::Error::new(
954 io::ErrorKind::InvalidData,
955 "CONNECT authority missing host",
956 ));
957 }
958 let port = port
959 .parse::<u16>()
960 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid CONNECT port"))?;
961 let expected_sni = host
962 .parse::<IpAddr>()
963 .is_err()
964 .then(|| host.to_ascii_lowercase());
965
966 Ok(ConnectTarget {
967 host: host.to_ascii_lowercase(),
968 port,
969 expected_sni,
970 })
971}
972
973fn is_http_version(version: &str) -> bool {
974 let Some(version) = version.strip_prefix("HTTP/") else {
975 return false;
976 };
977 let Some((major, minor)) = version.split_once('.') else {
978 return false;
979 };
980 !major.is_empty()
981 && !minor.is_empty()
982 && major.bytes().all(|b| b.is_ascii_digit())
983 && minor.bytes().all(|b| b.is_ascii_digit())
984}
985
986fn connect_response_is_success(headers: &[u8]) -> bool {
987 let Some(status_line) = headers.split(|&b| b == b'\n').next() else {
988 return false;
989 };
990 let Ok(status_line) = std::str::from_utf8(status_line) else {
991 return false;
992 };
993 let mut parts = status_line.trim_end_matches('\r').split_ascii_whitespace();
994 let version = parts.next().unwrap_or_default();
995 let status = parts.next().unwrap_or_default();
996 is_http_version(version)
997 && status.len() == 3
998 && status
999 .parse::<u16>()
1000 .is_ok_and(|code| (200..300).contains(&code))
1001}
1002
1003fn extract_http_host(buf: &[u8]) -> Option<String> {
1013 if buf.first() == Some(&0x16) {
1014 return None;
1015 }
1016 let mut headers = vec![httparse::EMPTY_HEADER; (buf.len() / 4).max(16)];
1022 let mut req = httparse::Request::new(&mut headers);
1023 req.parse(buf).ok()?;
1024 req.headers
1025 .iter()
1026 .find(|h| h.name.eq_ignore_ascii_case("host"))
1027 .and_then(|h| std::str::from_utf8(h.value).ok())
1028 .map(|v| {
1029 let host = v.trim();
1030 host.rsplit_once(':')
1032 .map(|(h, _)| h)
1033 .unwrap_or(host)
1034 .to_ascii_lowercase()
1035 })
1036 .filter(|h| !h.is_empty())
1037}
1038
1039#[allow(clippy::too_many_arguments)]
1056async fn classify_first_flight(
1057 mut buf: Vec<u8>,
1058 from_smoltcp: &mut mpsc::Receiver<Bytes>,
1059 server_rx: &mut tokio::net::tcp::OwnedReadHalf,
1060 to_smoltcp: &mpsc::Sender<Bytes>,
1061 shared: &SharedState,
1062 want_headers: bool,
1063 max: usize,
1064 budget: Duration,
1065) -> io::Result<(Vec<u8>, bool)> {
1066 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
1067 let timeout_fut = tokio::time::sleep(budget);
1068 tokio::pin!(timeout_fut);
1069
1070 loop {
1071 if !buf.is_empty() {
1077 let is_tls = buf.first() == Some(&0x16);
1078 let not_http = !is_tls
1079 && (!looks_like_http_request_prefix(&buf) || first_line_is_not_http_request(&buf));
1080 let done = !want_headers
1081 || is_tls
1082 || not_http
1083 || buf.len() >= max
1084 || buf.windows(4).any(|w| w == b"\r\n\r\n");
1085 if done {
1086 return Ok((buf, is_tls));
1087 }
1088 }
1089
1090 tokio::select! {
1091 biased;
1092 _ = &mut timeout_fut => {
1093 let is_tls = buf.first() == Some(&0x16);
1094 return Ok((buf, is_tls));
1095 }
1096 guest = from_smoltcp.recv() => match guest {
1099 Some(bytes) => buf.extend_from_slice(&bytes),
1100 None => {
1101 let is_tls = buf.first() == Some(&0x16);
1102 return Ok((buf, is_tls));
1103 }
1104 },
1105 server = server_rx.read(&mut server_buf) => match server {
1108 Ok(0) => {
1109 let is_tls = buf.first() == Some(&0x16);
1110 return Ok((buf, is_tls));
1111 }
1112 Ok(n) => {
1113 let data = Bytes::copy_from_slice(&server_buf[..n]);
1114 if to_smoltcp.send(data).await.is_err() {
1115 let is_tls = buf.first() == Some(&0x16);
1116 return Ok((buf, is_tls));
1117 }
1118 shared.proxy_wake.wake();
1119 }
1120 Err(e) => return Err(e),
1121 },
1122 }
1123 }
1124}
1125
1126async fn peek_for_sni(
1136 rx: &mut mpsc::Receiver<Bytes>,
1137 max: usize,
1138 budget: Duration,
1139) -> (Vec<u8>, Option<String>) {
1140 let mut buf = Vec::with_capacity(PEEK_BUF_SIZE.min(8192));
1141 let timeout_fut = tokio::time::sleep(budget);
1142 tokio::pin!(timeout_fut);
1143
1144 let raw_sni = loop {
1145 tokio::select! {
1146 biased;
1147 _ = &mut timeout_fut => break None,
1148 data = rx.recv() => {
1149 match data {
1150 Some(bytes) => {
1151 buf.extend_from_slice(&bytes);
1152 if buf.first() != Some(&0x16) {
1157 break None;
1158 }
1159 if let Some(name) = sni::extract_sni(&buf) {
1160 break Some(name);
1161 }
1162 if buf.len() >= max {
1163 break None;
1164 }
1165 }
1166 None => break None,
1167 }
1168 }
1169 }
1170 };
1171
1172 let canonical = raw_sni.map(|s| s.trim_end_matches('.').to_ascii_lowercase());
1173 (buf, canonical)
1174}
1175
1176#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 fn synthetic_client_hello(sni: &str) -> Vec<u8> {
1188 let host_bytes = sni.as_bytes();
1191 let host_len = host_bytes.len() as u16;
1192 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();
1197 body.extend_from_slice(&[0x03, 0x03]);
1199 body.extend_from_slice(&[0u8; 32]);
1201 body.push(0);
1203 body.extend_from_slice(&[0x00, 0x02, 0x00, 0x2f]);
1205 body.extend_from_slice(&[0x01, 0x00]);
1207 body.extend_from_slice(&extensions_total.to_be_bytes());
1209 body.extend_from_slice(&[0x00, 0x00]);
1211 body.extend_from_slice(&extension_data_len.to_be_bytes());
1212 body.extend_from_slice(&server_name_list_len.to_be_bytes());
1213 body.push(0x00); body.extend_from_slice(&host_len.to_be_bytes());
1215 body.extend_from_slice(host_bytes);
1216
1217 let handshake_len = body.len() as u32;
1218 let mut hs = Vec::new();
1219 hs.push(0x01); hs.extend_from_slice(&handshake_len.to_be_bytes()[1..]); hs.extend_from_slice(&body);
1222
1223 let record_len = hs.len() as u16;
1224 let mut record = Vec::new();
1225 record.extend_from_slice(&[0x16, 0x03, 0x01]); record.extend_from_slice(&record_len.to_be_bytes());
1227 record.extend_from_slice(&hs);
1228
1229 record
1230 }
1231
1232 #[tokio::test]
1233 async fn connect_upstream_dials_target_directly_without_outbound_proxy() {
1234 use tokio::net::TcpListener;
1235
1236 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1237 let addr = listener.local_addr().unwrap();
1238 let accept = tokio::spawn(async move {
1239 let (mut sock, _) = listener.accept().await.unwrap();
1240 let mut buf = [0u8; 5];
1241 sock.read_exact(&mut buf).await.unwrap();
1242 assert_eq!(&buf, b"hello");
1243 });
1244
1245 let shared = SharedState::new(4);
1246 let proxy_connect = ProxyConnectState::new();
1247 let mut stream = UpstreamTcpTarget::direct(addr)
1248 .connect(&proxy_connect, &shared, None)
1249 .await
1250 .unwrap();
1251 stream.write_all(b"hello").await.unwrap();
1252
1253 accept.await.unwrap();
1254 assert!(matches!(
1255 proxy_connect.status(),
1256 ProxyConnectStatus::Connected
1257 ));
1258 }
1259
1260 #[tokio::test]
1261 async fn early_http_connect_dials_proxy_through_configured_socks5_proxy() {
1262 let _ = rustls::crypto::ring::default_provider().install_default();
1263
1264 let http_proxy_addr: SocketAddr = "93.184.216.34:3128".parse().unwrap();
1267 let socks_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1268 let outbound_proxy = ResolvedOutboundProxy::Socks5 {
1269 address: socks_listener.local_addr().unwrap(),
1270 credentials: None,
1271 };
1272 let socks_task = tokio::spawn(async move {
1273 let (mut client, _) = socks_listener.accept().await.unwrap();
1274
1275 let mut greeting = [0u8; 3];
1276 client.read_exact(&mut greeting).await.unwrap();
1277 assert_eq!(greeting, [0x05, 0x01, 0x00]);
1278 client.write_all(&[0x05, 0x00]).await.unwrap();
1279
1280 let mut socks_request = [0u8; 10];
1281 client.read_exact(&mut socks_request).await.unwrap();
1282 assert_eq!(socks_request[0..4], [0x05, 0x01, 0x00, 0x01]);
1283 assert_eq!(&socks_request[4..8], &[93, 184, 216, 34]);
1284 assert_eq!(
1285 u16::from_be_bytes([socks_request[8], socks_request[9]]),
1286 3128
1287 );
1288 client
1289 .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1290 .await
1291 .unwrap();
1292
1293 let expected_connect =
1294 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
1295 let mut connect_request = vec![0u8; expected_connect.len()];
1296 client.read_exact(&mut connect_request).await.unwrap();
1297 assert_eq!(&connect_request, expected_connect);
1298 client
1299 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
1300 .await
1301 .unwrap();
1302 });
1303
1304 let connect_request =
1305 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n".to_vec();
1306 let (from_tx, from_rx) = mpsc::channel(1);
1307 let (to_tx, mut to_rx) = mpsc::channel(1);
1308 drop(from_tx);
1309
1310 let tls_state = Arc::new(
1311 TlsState::new(
1312 microsandbox_types::TlsConfig::default(),
1313 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1314 )
1315 .unwrap(),
1316 );
1317 let proxy_connect = Arc::new(ProxyConnectState::new());
1318
1319 handle_connect_tunnel(
1320 http_proxy_addr,
1321 UpstreamTcpTarget::direct(http_proxy_addr),
1322 connect_request,
1323 from_rx,
1324 to_tx,
1325 Arc::new(SharedState::new(4)),
1326 Arc::new(NetworkPolicy::default()),
1327 tls_state,
1328 false,
1329 proxy_connect.clone(),
1330 Some(Arc::new(outbound_proxy)),
1331 None,
1332 )
1333 .await
1334 .unwrap();
1335
1336 let response = to_rx.recv().await.unwrap();
1337 assert_eq!(
1338 &response[..],
1339 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1340 );
1341 socks_task.await.unwrap();
1342 assert!(matches!(
1343 proxy_connect.status(),
1344 ProxyConnectStatus::Connected
1345 ));
1346 }
1347
1348 #[test]
1349 fn could_be_connect_request_matches_split_prefixes_only() {
1350 assert!(could_be_connect_request(b"C"));
1351 assert!(could_be_connect_request(b"connect "));
1352 assert!(could_be_connect_request(b"CONNECT example.com:443"));
1353 assert!(!could_be_connect_request(b"CLIENT"));
1354 assert!(!could_be_connect_request(b"GET / HTTP/1.1\r\n"));
1355 }
1356
1357 #[tokio::test]
1358 async fn buffer_connect_request_reads_split_headers() {
1359 let (tx, mut rx) = mpsc::channel(4);
1360 tx.send(Bytes::from_static(b"NECT example.com:443 HTTP/1.1\r\n"))
1361 .await
1362 .unwrap();
1363 tx.send(Bytes::from_static(b"Host: example.com\r\n\r\n"))
1364 .await
1365 .unwrap();
1366 drop(tx);
1367
1368 let buffered = buffer_connect_request(b"CON".to_vec(), &mut rx)
1369 .await
1370 .unwrap();
1371 let parsed = parse_connect_request(buffered).unwrap();
1372
1373 assert_eq!(parsed.target.host, "example.com");
1374 assert_eq!(parsed.target.port, 443);
1375 assert_eq!(parsed.target.expected_sni.as_deref(), Some("example.com"));
1376 assert!(parsed.post_header_bytes().is_empty());
1377 }
1378
1379 #[test]
1380 fn parse_connect_request_preserves_post_header_tls_seed() {
1381 let mut request = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
1382 request.extend_from_slice(b"\x16\x03\x01client-hello");
1383
1384 let parsed = parse_connect_request(request).unwrap();
1385
1386 assert_eq!(
1387 parsed.header_bytes(),
1388 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n"
1389 );
1390 assert_eq!(parsed.post_header_bytes(), b"\x16\x03\x01client-hello");
1391 }
1392
1393 #[test]
1394 fn parse_connect_target_requires_authority_port() {
1395 assert!(parse_connect_target("example.com").is_err());
1396 assert!(parse_connect_target("2001:db8::1:443").is_err());
1397
1398 let target = parse_connect_target("[2001:db8::1]:8443").unwrap();
1399 assert_eq!(target.host, "2001:db8::1");
1400 assert_eq!(target.port, 8443);
1401 assert_eq!(target.expected_sni, None);
1402 }
1403
1404 #[test]
1405 fn connect_response_success_requires_exact_2xx_status_code() {
1406 assert!(connect_response_is_success(
1407 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1408 ));
1409 assert!(connect_response_is_success(
1410 b"HTTP/1.1 204 Connection Established\r\n\r\n"
1411 ));
1412 assert!(!connect_response_is_success(b"HTTP/1.1 2000 Weird\r\n\r\n"));
1413 assert!(!connect_response_is_success(b"HTTP/1.1 199 Nope\r\n\r\n"));
1414 assert!(!connect_response_is_success(b"NOTHTTP 200 OK\r\n\r\n"));
1415 }
1416
1417 #[tokio::test]
1418 async fn peek_for_sni_extracts_and_canonicalizes() {
1419 let (tx, mut rx) = mpsc::channel(4);
1420 let hello = synthetic_client_hello("Example.COM");
1421 tx.send(Bytes::from(hello.clone())).await.unwrap();
1422 drop(tx); let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1425 assert_eq!(sni.as_deref(), Some("example.com"));
1426 assert_eq!(buf, hello);
1427 }
1428
1429 #[tokio::test]
1430 async fn peek_for_sni_returns_none_on_channel_close_without_data() {
1431 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1432 drop(tx);
1433 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1434 assert!(buf.is_empty());
1435 assert_eq!(sni, None);
1436 }
1437
1438 #[tokio::test]
1439 async fn peek_for_sni_returns_none_on_non_tls_data() {
1440 let (tx, mut rx) = mpsc::channel(4);
1441 tx.send(Bytes::from_static(
1443 b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
1444 ))
1445 .await
1446 .unwrap();
1447 drop(tx);
1448 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1449 assert!(
1450 !buf.is_empty(),
1451 "buffered bytes must be returned for replay"
1452 );
1453 assert_eq!(sni, None);
1454 }
1455
1456 #[tokio::test]
1457 async fn peek_for_sni_falls_back_on_timeout() {
1458 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1459 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, Duration::from_millis(50)).await;
1461 drop(tx);
1462 assert!(buf.is_empty());
1463 assert_eq!(sni, None);
1464 }
1465
1466 #[tokio::test]
1467 async fn peek_for_sni_caps_at_max_bytes() {
1468 let (tx, mut rx) = mpsc::channel(4);
1469 let mut first = vec![0u8; 8192];
1473 first[0] = 0x16;
1474 tx.send(Bytes::from(first)).await.unwrap();
1475 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1476 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1477 drop(tx);
1478
1479 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1480 assert_eq!(sni, None, "no SNI in non-TLS data");
1481 assert!(
1482 buf.len() >= PEEK_BUF_SIZE,
1483 "buffer must hit the cap before bail-out: got {}",
1484 buf.len()
1485 );
1486 }
1487
1488 #[tokio::test]
1489 async fn peek_for_sni_bails_immediately_on_non_tls_first_byte() {
1490 let (tx, mut rx) = mpsc::channel(4);
1491 tx.send(Bytes::from_static(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
1493 .await
1494 .unwrap();
1495 drop(tx);
1496
1497 let started = std::time::Instant::now();
1500 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1501 let elapsed = started.elapsed();
1502 assert_eq!(sni, None);
1503 assert!(buf.starts_with(b"GET"));
1504 assert!(
1505 elapsed < Duration::from_millis(500),
1506 "non-TLS bail must be fast: took {elapsed:?}"
1507 );
1508 }
1509
1510 use std::net::IpAddr;
1515 use std::time::Duration as StdDuration;
1516
1517 use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
1518 use crate::policy::{Action, Destination, NetworkPolicy, PortRange, Rule};
1519
1520 const SHARED_FASTLY_IP: &str = "151.101.0.223";
1521
1522 fn shared_with(host: &str, ip: &str) -> SharedState {
1523 let shared = SharedState::new(4);
1524 shared.cache_resolved_hostname(
1525 host,
1526 ResolvedHostnameFamily::Ipv4,
1527 [ip.parse::<IpAddr>().unwrap()],
1528 StdDuration::from_secs(60),
1529 );
1530 shared
1531 }
1532
1533 fn allow_https(domain: &str) -> Rule {
1534 Rule {
1535 direction: crate::policy::Direction::Egress,
1536 destination: Destination::Domain(domain.parse().unwrap()),
1537 protocols: vec![Protocol::Tcp],
1538 ports: vec![PortRange::single(443)],
1539 action: Action::Allow,
1540 }
1541 }
1542
1543 fn allow_tcp(domain: &str, port: u16) -> Rule {
1544 Rule {
1545 direction: crate::policy::Direction::Egress,
1546 destination: Destination::Domain(domain.parse().unwrap()),
1547 protocols: vec![Protocol::Tcp],
1548 ports: vec![PortRange::single(port)],
1549 action: Action::Allow,
1550 }
1551 }
1552
1553 #[tokio::test]
1556 async fn integration_sni_overrides_cache_for_over_allow() {
1557 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1558 let policy = NetworkPolicy {
1559 default_egress: Action::Deny,
1560 default_ingress: Action::Allow,
1561 rules: vec![allow_https("pypi.org")],
1562 };
1563 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1564
1565 let (tx, mut rx) = mpsc::channel(4);
1566 tx.send(Bytes::from(synthetic_client_hello("evil.com")))
1567 .await
1568 .unwrap();
1569 drop(tx);
1570
1571 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1572 assert_eq!(sni.as_deref(), Some("evil.com"));
1573 assert!(!initial_buf.is_empty());
1574
1575 let source = sni
1576 .as_deref()
1577 .map(HostnameSource::Sni)
1578 .unwrap_or(HostnameSource::CacheOnly);
1579 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1580 assert_eq!(
1581 eval,
1582 EgressEvaluation::Deny,
1583 "SNI=evil.com must not piggy-back on the cached pypi.org match",
1584 );
1585 }
1586
1587 #[tokio::test]
1590 async fn integration_sni_overrides_cache_for_over_block() {
1591 let shared = shared_with("ads.example.com", SHARED_FASTLY_IP);
1592 let policy = NetworkPolicy {
1593 default_egress: Action::Allow,
1594 default_ingress: Action::Allow,
1595 rules: vec![Rule::deny_egress(Destination::Domain(
1596 "ads.example.com".parse().unwrap(),
1597 ))],
1598 };
1599 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1600
1601 let (tx, mut rx) = mpsc::channel(4);
1602 tx.send(Bytes::from(synthetic_client_hello("api.example.com")))
1603 .await
1604 .unwrap();
1605 drop(tx);
1606
1607 let (_initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1608 assert_eq!(sni.as_deref(), Some("api.example.com"));
1609
1610 let source = sni
1611 .as_deref()
1612 .map(HostnameSource::Sni)
1613 .unwrap_or(HostnameSource::CacheOnly);
1614 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1615 assert_eq!(
1616 eval,
1617 EgressEvaluation::Allow,
1618 "SNI=api.example.com must not be caught by the deny on ads.example.com",
1619 );
1620 }
1621
1622 #[tokio::test]
1625 async fn integration_non_tls_falls_back_to_cache() {
1626 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1627 let policy = NetworkPolicy {
1628 default_egress: Action::Deny,
1629 default_ingress: Action::Allow,
1630 rules: vec![allow_https("pypi.org")],
1631 };
1632 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1633
1634 let (tx, mut rx) = mpsc::channel(4);
1635 tx.send(Bytes::from_static(
1637 b"GET / HTTP/1.1\r\nHost: pypi.org\r\n\r\n",
1638 ))
1639 .await
1640 .unwrap();
1641 drop(tx);
1642
1643 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1644 assert_eq!(sni, None, "non-TLS data → no SNI");
1645 assert!(
1646 !initial_buf.is_empty(),
1647 "buffered bytes must survive for replay"
1648 );
1649
1650 let source = sni
1651 .as_deref()
1652 .map(HostnameSource::Sni)
1653 .unwrap_or(HostnameSource::CacheOnly);
1654 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1655 assert_eq!(
1656 eval,
1657 EgressEvaluation::Allow,
1658 "cache-only fallback must still allow the cached hostname's IP",
1659 );
1660 }
1661
1662 #[tokio::test]
1665 async fn integration_sni_matches_domain_suffix_with_cache_binding() {
1666 let shared = shared_with("files.pythonhosted.org", SHARED_FASTLY_IP);
1667 let policy = NetworkPolicy {
1668 default_egress: Action::Deny,
1669 default_ingress: Action::Allow,
1670 rules: vec![Rule {
1671 direction: crate::policy::Direction::Egress,
1672 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1673 protocols: vec![Protocol::Tcp],
1674 ports: vec![PortRange::single(443)],
1675 action: Action::Allow,
1676 }],
1677 };
1678 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1679
1680 let (tx, mut rx) = mpsc::channel(4);
1681 tx.send(Bytes::from(synthetic_client_hello(
1682 "files.pythonhosted.org",
1683 )))
1684 .await
1685 .unwrap();
1686 drop(tx);
1687
1688 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1689 let source = sni
1690 .as_deref()
1691 .map(HostnameSource::Sni)
1692 .unwrap_or(HostnameSource::CacheOnly);
1693 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1694 assert_eq!(eval, EgressEvaluation::Allow);
1695 }
1696
1697 #[tokio::test]
1702 async fn integration_sni_denies_domain_suffix_without_cache_binding() {
1703 let shared = SharedState::new(4); let policy = NetworkPolicy {
1705 default_egress: Action::Deny,
1706 default_ingress: Action::Allow,
1707 rules: vec![Rule {
1708 direction: crate::policy::Direction::Egress,
1709 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1710 protocols: vec![Protocol::Tcp],
1711 ports: vec![PortRange::single(443)],
1712 action: Action::Allow,
1713 }],
1714 };
1715 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1716
1717 let (tx, mut rx) = mpsc::channel(4);
1718 tx.send(Bytes::from(synthetic_client_hello(
1719 "files.pythonhosted.org",
1720 )))
1721 .await
1722 .unwrap();
1723 drop(tx);
1724
1725 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1726 let source = sni
1727 .as_deref()
1728 .map(HostnameSource::Sni)
1729 .unwrap_or(HostnameSource::CacheOnly);
1730 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1731 assert_eq!(eval, EgressEvaluation::Deny);
1732 }
1733
1734 #[test]
1737 fn extract_http_host_basic() {
1738 let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1739 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1740 }
1741
1742 #[test]
1743 fn extract_http_host_strips_port() {
1744 let buf = b"POST /api HTTP/1.1\r\nHost: api.company.com:8080\r\n\r\n";
1745 assert_eq!(extract_http_host(buf), Some("api.company.com".into()));
1746 }
1747
1748 #[test]
1749 fn extract_http_host_case_insensitive_lowercased() {
1750 let buf = b"GET / HTTP/1.1\r\nhost: Example.COM\r\n\r\n";
1751 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1752 }
1753
1754 #[test]
1755 fn extract_http_host_no_host_header() {
1756 let buf = b"GET / HTTP/1.1\r\nX-Other: foo\r\n\r\n";
1757 assert_eq!(extract_http_host(buf), None);
1758 }
1759
1760 #[test]
1761 fn extract_http_host_incomplete_headers() {
1762 let buf = b"GET / HTTP/1.1\r\nHost: x";
1763 assert_eq!(extract_http_host(buf), None);
1764 }
1765
1766 #[test]
1767 fn extract_http_host_tls_first_byte() {
1768 let buf = [0x16u8, 0x03, 0x01, 0x00, 0x01];
1769 assert_eq!(extract_http_host(&buf), None);
1770 }
1771
1772 #[test]
1773 fn extract_http_host_with_many_headers() {
1774 let mut req = Vec::from(&b"GET / HTTP/1.1\r\n"[..]);
1777 for i in 0..100 {
1778 req.extend_from_slice(format!("X-Pad-{i}: v\r\n").as_bytes());
1779 }
1780 req.extend_from_slice(b"Host: example.com\r\n\r\n");
1781 assert_eq!(extract_http_host(&req), Some("example.com".into()));
1782 }
1783
1784 use std::sync::Arc;
1787 use tokio::io::AsyncReadExt;
1788 use tokio::net::TcpListener;
1789 use tokio::task::JoinHandle;
1790
1791 use crate::secrets::config::{
1792 HostPattern, SecretEntry, SecretSubstitution, SecretViolationAction, SecretsConfig,
1793 };
1794
1795 fn make_plain_http_secret(placeholder: &str, value: &str, require_tls: bool) -> SecretsConfig {
1796 SecretsConfig {
1797 secrets: vec![SecretEntry {
1798 env_var: "API_KEY".into(),
1799 value: zeroize::Zeroizing::new(value.into()),
1800 source: None,
1801 placeholder: placeholder.into(),
1802 allowed_hosts: vec![HostPattern::Any],
1803 substitution: SecretSubstitution {
1804 headers: true,
1805 query: false,
1806 body: false,
1807 },
1808 passthrough_hosts: Vec::new(),
1809 violation_action: None,
1810 require_tls_identity: require_tls,
1811 }],
1812 ..Default::default()
1813 }
1814 }
1815
1816 fn make_host_bound_secret(placeholder: &str, value: &str, host: &str) -> SecretsConfig {
1817 SecretsConfig {
1818 secrets: vec![SecretEntry {
1819 env_var: "API_KEY".into(),
1820 value: zeroize::Zeroizing::new(value.into()),
1821 source: None,
1822 placeholder: placeholder.into(),
1823 allowed_hosts: vec![HostPattern::Exact(host.into())],
1824 substitution: SecretSubstitution::default(),
1825 passthrough_hosts: Vec::new(),
1826 violation_action: None,
1827 require_tls_identity: true,
1828 }],
1829 ..Default::default()
1830 }
1831 }
1832
1833 #[test]
1834 fn sanitize_connect_headers_blocks_placeholder_metadata_header_by_default() {
1835 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1836 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";
1837
1838 assert_eq!(
1839 sanitize_connect_headers(headers, &secrets),
1840 Err(SecretViolationAction::BlockAndLog)
1841 );
1842 }
1843
1844 #[test]
1845 fn sanitize_connect_headers_respects_block_and_terminate() {
1846 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1847 secrets.violation_action = SecretViolationAction::BlockAndTerminate;
1848 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";
1849
1850 assert_eq!(
1851 sanitize_connect_headers(headers, &secrets),
1852 Err(SecretViolationAction::BlockAndTerminate)
1853 );
1854 }
1855
1856 #[test]
1857 fn sanitize_connect_headers_respects_explicit_passthrough() {
1858 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1859 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
1860 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";
1861
1862 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1863
1864 assert_eq!(sanitized.as_ref(), headers);
1865 assert!(
1866 !String::from_utf8_lossy(sanitized.as_ref()).contains("real-secret-value"),
1867 "passthrough must never substitute real secrets into CONNECT metadata"
1868 );
1869 }
1870
1871 #[test]
1872 fn sanitize_connect_headers_keeps_safe_metadata_headers() {
1873 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1874 let headers =
1875 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nUser-Agent: curl\r\n\r\n";
1876
1877 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1878
1879 assert_eq!(sanitized.as_ref(), headers);
1880 }
1881
1882 #[test]
1883 fn sanitize_connect_headers_blocks_placeholder_in_request_line() {
1884 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1885 let headers = b"CONNECT $MSB_KEY:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
1886
1887 assert_eq!(
1888 sanitize_connect_headers(headers, &secrets),
1889 Err(SecretViolationAction::BlockAndLog)
1890 );
1891 }
1892
1893 async fn spawn_sink() -> (SocketAddr, JoinHandle<Vec<u8>>) {
1894 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1895 let addr = listener.local_addr().unwrap();
1896 let handle = tokio::spawn(async move {
1897 let (mut stream, _) = listener.accept().await.unwrap();
1898 let mut received = Vec::new();
1899 let mut buf = vec![0u8; 4096];
1900 loop {
1901 match stream.read(&mut buf).await {
1902 Ok(0) | Err(_) => break,
1903 Ok(n) => received.extend_from_slice(&buf[..n]),
1904 }
1905 }
1906 received
1907 });
1908 (addr, handle)
1909 }
1910
1911 async fn relay_through_proxy(
1912 request: Vec<u8>,
1913 secrets: SecretsConfig,
1914 handle: JoinHandle<Vec<u8>>,
1915 server_addr: SocketAddr,
1916 ) -> Vec<u8> {
1917 relay_through_proxy_with_policy(
1918 request,
1919 Arc::new(SharedState::new(4)),
1920 Arc::new(NetworkPolicy::default()),
1921 secrets,
1922 handle,
1923 server_addr,
1924 )
1925 .await
1926 }
1927
1928 async fn relay_through_proxy_with_policy(
1929 request: Vec<u8>,
1930 shared: Arc<SharedState>,
1931 policy: Arc<NetworkPolicy>,
1932 secrets: SecretsConfig,
1933 handle: JoinHandle<Vec<u8>>,
1934 server_addr: SocketAddr,
1935 ) -> Vec<u8> {
1936 relay_chunks_through_proxy_with_policy(
1937 vec![request],
1938 shared,
1939 policy,
1940 secrets,
1941 handle,
1942 server_addr,
1943 )
1944 .await
1945 }
1946
1947 async fn relay_chunks_through_proxy_with_policy(
1948 chunks: Vec<Vec<u8>>,
1949 shared: Arc<SharedState>,
1950 policy: Arc<NetworkPolicy>,
1951 secrets: SecretsConfig,
1952 handle: JoinHandle<Vec<u8>>,
1953 server_addr: SocketAddr,
1954 ) -> Vec<u8> {
1955 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1956 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
1957 let secrets = Arc::new(secrets);
1958 let proxy_connect = Arc::new(ProxyConnectState::new());
1959
1960 for chunk in chunks {
1961 from_tx.send(Bytes::from(chunk)).await.unwrap();
1962 }
1963 drop(from_tx);
1964
1965 TcpProxy::new(
1966 server_addr,
1967 UpstreamTcpTarget::direct(server_addr),
1968 from_rx,
1969 to_tx,
1970 shared,
1971 policy,
1972 secrets,
1973 None,
1974 false,
1975 proxy_connect,
1976 None,
1977 )
1978 .try_run()
1979 .await
1980 .unwrap();
1981
1982 handle.await.unwrap()
1983 }
1984
1985 #[tokio::test]
1986 async fn plain_http_domain_policy_allows_matching_host() {
1987 let (addr, sink) = spawn_sink().await;
1988 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
1989 let policy = Arc::new(NetworkPolicy {
1990 default_egress: Action::Deny,
1991 default_ingress: Action::Allow,
1992 rules: vec![allow_tcp("allowed.example", addr.port())],
1993 });
1994
1995 let wire = relay_through_proxy_with_policy(
1996 b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
1997 shared,
1998 policy,
1999 SecretsConfig::default(),
2000 sink,
2001 addr,
2002 )
2003 .await;
2004
2005 assert_eq!(wire, b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2006 }
2007
2008 #[tokio::test]
2009 async fn plain_http_domain_policy_blocks_host_switch() {
2010 let (addr, sink) = spawn_sink().await;
2011 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2012 let policy = Arc::new(NetworkPolicy {
2013 default_egress: Action::Deny,
2014 default_ingress: Action::Allow,
2015 rules: vec![allow_tcp("allowed.example", addr.port())],
2016 });
2017
2018 let wire = relay_through_proxy_with_policy(
2019 b"GET / HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2020 shared,
2021 policy,
2022 SecretsConfig::default(),
2023 sink,
2024 addr,
2025 )
2026 .await;
2027
2028 assert!(
2029 wire.is_empty(),
2030 "switched HTTP authority must not reach upstream, got: {wire:?}"
2031 );
2032 }
2033
2034 #[tokio::test]
2035 async fn plain_http_domain_policy_blocks_keep_alive_host_switch() {
2036 let (addr, sink) = spawn_sink().await;
2037 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2038 let policy = Arc::new(NetworkPolicy {
2039 default_egress: Action::Deny,
2040 default_ingress: Action::Allow,
2041 rules: vec![allow_tcp("allowed.example", addr.port())],
2042 });
2043
2044 let wire = relay_chunks_through_proxy_with_policy(
2045 vec![
2046 b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2047 b"GET /two HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2048 ],
2049 shared,
2050 policy,
2051 SecretsConfig::default(),
2052 sink,
2053 addr,
2054 )
2055 .await;
2056
2057 assert_eq!(wire, b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2058 }
2059
2060 #[test]
2061 fn strict_hostname_allow_blocks_sni_authority_before_tcp_dial() {
2062 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2063 let shared = shared_with("allowed.example", "127.0.0.1");
2064 let policy = NetworkPolicy {
2065 default_egress: Action::Deny,
2066 default_ingress: Action::Allow,
2067 rules: vec![allow_tcp("allowed.example", dst.port())],
2068 };
2069
2070 assert!(strict_hostname_allow_is_opaque(
2071 true,
2072 &policy,
2073 dst,
2074 &shared,
2075 Some("allowed.example"),
2076 &synthetic_client_hello("allowed.example"),
2077 ));
2078 }
2079
2080 #[test]
2081 fn strict_hostname_allow_blocks_tls_without_sni_before_tcp_dial() {
2082 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2083 let shared = shared_with("allowed.example", "127.0.0.1");
2084 let policy = NetworkPolicy {
2085 default_egress: Action::Deny,
2086 default_ingress: Action::Allow,
2087 rules: vec![allow_tcp("allowed.example", dst.port())],
2088 };
2089
2090 assert!(strict_hostname_allow_is_opaque(
2091 true,
2092 &policy,
2093 dst,
2094 &shared,
2095 None,
2096 &[0x16, 0x03, 0x01],
2097 ));
2098 }
2099
2100 #[test]
2101 fn strict_hostname_allow_leaves_plain_http_for_authority_validation() {
2102 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 80);
2103 let shared = shared_with("allowed.example", "127.0.0.1");
2104 let policy = NetworkPolicy {
2105 default_egress: Action::Deny,
2106 default_ingress: Action::Allow,
2107 rules: vec![allow_tcp("allowed.example", dst.port())],
2108 };
2109
2110 assert!(!strict_hostname_allow_is_opaque(
2111 true,
2112 &policy,
2113 dst,
2114 &shared,
2115 None,
2116 b"GET / HTTP/1.1\r\n",
2117 ));
2118 }
2119
2120 #[tokio::test]
2121 async fn strict_mode_blocks_hostname_allowed_opaque_tls() {
2122 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2123 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2124 let policy = Arc::new(NetworkPolicy {
2125 default_egress: Action::Deny,
2126 default_ingress: Action::Allow,
2127 rules: vec![allow_tcp("allowed.example", dst.port())],
2128 });
2129 let proxy_connect = Arc::new(ProxyConnectState::new());
2130 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2131 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2132
2133 from_tx
2134 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2135 .await
2136 .unwrap();
2137 drop(from_tx);
2138
2139 TcpProxy::new(
2140 dst,
2141 UpstreamTcpTarget::direct(dst),
2142 from_rx,
2143 to_tx,
2144 shared,
2145 policy,
2146 Arc::new(SecretsConfig::default()),
2147 None,
2148 true,
2149 proxy_connect.clone(),
2150 None,
2151 )
2152 .try_run()
2153 .await
2154 .unwrap();
2155
2156 assert_eq!(proxy_connect.status(), ProxyConnectStatus::PolicyDenied);
2157 }
2158
2159 #[tokio::test]
2160 async fn strict_mode_leaves_default_allowed_opaque_tls_to_policy() {
2161 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 9);
2162 let shared = Arc::new(SharedState::new(4));
2163 let policy = Arc::new(NetworkPolicy {
2164 default_egress: Action::Allow,
2165 default_ingress: Action::Allow,
2166 rules: vec![Rule::deny_egress(Destination::Domain(
2167 "blocked.example".parse().unwrap(),
2168 ))],
2169 });
2170 let proxy_connect = Arc::new(ProxyConnectState::new());
2171 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2172 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2173
2174 from_tx
2175 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2176 .await
2177 .unwrap();
2178 drop(from_tx);
2179
2180 let result = TcpProxy::new(
2181 dst,
2182 UpstreamTcpTarget::direct(dst),
2183 from_rx,
2184 to_tx,
2185 shared,
2186 policy,
2187 Arc::new(SecretsConfig::default()),
2188 None,
2189 true,
2190 proxy_connect.clone(),
2191 None,
2192 )
2193 .try_run()
2194 .await;
2195
2196 assert!(result.is_err(), "dummy upstream should refuse the dial");
2197 assert_eq!(
2198 proxy_connect.status(),
2199 ProxyConnectStatus::UpstreamConnectFailed
2200 );
2201 }
2202
2203 #[tokio::test]
2204 async fn plain_http_substitutes_placeholder_when_host_arrives_in_second_segment() {
2205 let (addr, sink) = spawn_sink().await;
2208 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2209
2210 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2211 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2212 let proxy_connect = Arc::new(ProxyConnectState::new());
2213
2214 from_tx
2215 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2216 .await
2217 .unwrap();
2218 from_tx
2219 .send(Bytes::from_static(
2220 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2221 ))
2222 .await
2223 .unwrap();
2224 drop(from_tx);
2225
2226 TcpProxy::new(
2227 addr,
2228 UpstreamTcpTarget::direct(addr),
2229 from_rx,
2230 to_tx,
2231 Arc::new(SharedState::new(4)),
2232 Arc::new(NetworkPolicy::default()),
2233 Arc::new(secrets),
2234 None,
2235 false,
2236 proxy_connect,
2237 None,
2238 )
2239 .try_run()
2240 .await
2241 .unwrap();
2242
2243 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2244 assert!(wire.contains("real-secret-value"), "got: {wire:?}");
2245 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2246 }
2247
2248 #[tokio::test]
2249 async fn plain_http_passthrough_handles_a_host_in_split_headers() {
2250 let (addr, sink) = spawn_sink().await;
2254
2255 let shared = SharedState::new(4);
2256 shared.cache_resolved_hostname(
2257 "example.com",
2258 ResolvedHostnameFamily::Ipv4,
2259 ["127.0.0.1".parse::<IpAddr>().unwrap()],
2260 StdDuration::from_secs(60),
2261 );
2262
2263 let secrets = SecretsConfig {
2264 secrets: vec![SecretEntry {
2265 env_var: "API_KEY".into(),
2266 value: zeroize::Zeroizing::new("real-secret-value".into()),
2267 source: None,
2268 placeholder: "$MSB_KEY".into(),
2269 allowed_hosts: vec![HostPattern::Exact("example.com".into())],
2270 substitution: SecretSubstitution {
2271 headers: true,
2272 query: false,
2273 body: false,
2274 },
2275 passthrough_hosts: vec![HostPattern::Exact("example.com".into())],
2276 violation_action: None,
2277 require_tls_identity: true,
2278 }],
2279 ..Default::default()
2280 };
2281
2282 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2283 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2284 let proxy_connect = Arc::new(ProxyConnectState::new());
2285
2286 from_tx
2287 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2288 .await
2289 .unwrap();
2290 from_tx
2291 .send(Bytes::from_static(
2292 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2293 ))
2294 .await
2295 .unwrap();
2296 drop(from_tx);
2297
2298 TcpProxy::new(
2299 addr,
2300 UpstreamTcpTarget::direct(addr),
2301 from_rx,
2302 to_tx,
2303 Arc::new(shared),
2304 Arc::new(NetworkPolicy::default()),
2305 Arc::new(secrets),
2306 None,
2307 false,
2308 proxy_connect,
2309 None,
2310 )
2311 .try_run()
2312 .await
2313 .unwrap();
2314
2315 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2316 assert!(
2317 wire.contains("Host: example.com"),
2318 "request must reach the allowed host, got: {wire:?}"
2319 );
2320 assert!(
2321 wire.contains("$MSB_KEY"),
2322 "placeholder must be forwarded unchanged for a require_tls_identity secret, got: {wire:?}"
2323 );
2324 assert!(
2325 !wire.contains("real-secret-value"),
2326 "secret must never be substituted over plain HTTP, got: {wire:?}"
2327 );
2328 }
2329
2330 #[tokio::test]
2331 async fn plain_http_substitutes_placeholder_in_first_flight() {
2332 let (addr, sink) = spawn_sink().await;
2333
2334 let request =
2335 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2336 .to_vec();
2337 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2338
2339 let wire =
2340 String::from_utf8(relay_through_proxy(request, secrets, sink, addr).await).unwrap();
2341 assert!(
2342 wire.contains("real-secret-value"),
2343 "real value must reach server, got: {wire:?}"
2344 );
2345 assert!(
2346 !wire.contains("$MSB_KEY"),
2347 "placeholder must not reach server, got: {wire:?}"
2348 );
2349 }
2350
2351 #[tokio::test]
2352 async fn plain_http_no_substitution_when_require_tls_identity_true() {
2353 let (addr, sink) = spawn_sink().await;
2354
2355 let request =
2356 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2357 .to_vec();
2358 let mut secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", true);
2359 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
2360
2361 let wire =
2362 String::from_utf8_lossy(&relay_through_proxy(request, secrets, sink, addr).await)
2363 .into_owned();
2364 assert!(
2365 wire.contains("$MSB_KEY"),
2366 "placeholder must be forwarded unchanged when require_tls_identity=true, got: {wire:?}"
2367 );
2368 assert!(
2369 !wire.contains("real-secret-value"),
2370 "real value must not leak when require_tls_identity=true, got: {wire:?}"
2371 );
2372 }
2373
2374 #[tokio::test]
2375 async fn plain_http_large_body_forwarded_verbatim_in_relay_loop() {
2376 let (addr, sink) = spawn_sink().await;
2380 let secrets = make_plain_http_secret("$MSB_KEY", "real-value", false);
2381
2382 let body = "x".repeat(32_000);
2383 let header = format!(
2384 "POST /upload HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\nContent-Length: {}\r\n\r\n",
2385 body.len()
2386 );
2387
2388 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2389 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2390 let proxy_connect = Arc::new(ProxyConnectState::new());
2391
2392 from_tx
2393 .send(Bytes::from(header.into_bytes()))
2394 .await
2395 .unwrap();
2396 from_tx
2397 .send(Bytes::from(body.clone().into_bytes()))
2398 .await
2399 .unwrap();
2400 drop(from_tx);
2401
2402 TcpProxy::new(
2403 addr,
2404 UpstreamTcpTarget::direct(addr),
2405 from_rx,
2406 to_tx,
2407 Arc::new(SharedState::new(4)),
2408 Arc::new(NetworkPolicy::default()),
2409 Arc::new(secrets),
2410 None,
2411 false,
2412 proxy_connect,
2413 None,
2414 )
2415 .try_run()
2416 .await
2417 .unwrap();
2418
2419 let wire = String::from_utf8_lossy(&sink.await.unwrap()).into_owned();
2420 assert!(wire.contains(&body), "got {} bytes", wire.len());
2421 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2422 }
2423}