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 hostname_policy_deferred = match network_policy.evaluate_egress_with_source(
200 guest_dst,
201 Protocol::Tcp,
202 &shared,
203 HostnameSource::Deferred,
204 ) {
205 EgressEvaluation::Allow => false,
206 EgressEvaluation::DeferUntilHostname => true,
207 EgressEvaluation::Deny => network_policy.has_domain_rules(),
210 };
211
212 let (initial_buf, sni) = if hostname_policy_deferred {
218 peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await
219 } else {
220 (Vec::new(), None)
221 };
222
223 if hostname_policy_deferred {
229 let source = match sni.as_deref() {
230 Some(name) => HostnameSource::Sni(name),
231 None => HostnameSource::CacheOnly,
232 };
233 match network_policy.evaluate_egress_with_source(
234 guest_dst,
235 Protocol::Tcp,
236 &shared,
237 source,
238 ) {
239 EgressEvaluation::Allow => {
240 if strict_hostname_allow_is_opaque(
241 strict,
242 &network_policy,
243 guest_dst,
244 &shared,
245 sni.as_deref(),
246 &initial_buf,
247 ) {
248 tracing::debug!(
249 sni = sni.as_deref(),
250 dst = %guest_dst,
251 "TCP egress denied by strict hostname policy",
252 );
253 proxy_connect.mark_policy_denied();
254 shared.proxy_wake.wake();
255 return Ok(());
256 }
257 }
258 EgressEvaluation::Deny => {
259 tracing::debug!(
260 dst = %guest_dst,
261 source = source.label(),
262 "TCP egress denied by domain policy",
263 );
264 proxy_connect.mark_policy_denied();
265 shared.proxy_wake.wake();
266 return Ok(());
267 }
268 EgressEvaluation::DeferUntilHostname => {
269 debug_assert!(false, "DeferUntilHostname leaked into TCP proxy task");
270 proxy_connect.mark_policy_denied();
271 shared.proxy_wake.wake();
272 return Ok(());
273 }
274 }
275 }
276
277 if let Some(tls_state) = tls_state.clone()
281 && !initial_buf.is_empty()
282 && could_be_connect_request(&initial_buf)
283 {
284 return handle_connect_tunnel(
285 guest_dst,
286 connect_target,
287 initial_buf,
288 from_smoltcp,
289 to_smoltcp,
290 shared,
291 network_policy,
292 tls_state,
293 strict,
294 proxy_connect,
295 outbound_proxy,
296 None,
297 )
298 .await;
299 }
300
301 let stream = connect_target
306 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
307 .await?;
308 let connect_dst = stream.peer_addr().unwrap_or(connect_target.primary());
309 let (mut server_rx, mut server_tx) = stream.into_split();
310
311 let enforce_http_authority = network_policy.has_domain_rules();
317 let want_headers = enforce_http_authority
318 || secrets.has_plain_http_candidates()
319 || secrets.has_host_scoped_secrets();
320 let (initial_buf, is_tls) = if want_headers {
321 classify_first_flight(
322 initial_buf,
323 &mut from_smoltcp,
324 &mut server_rx,
325 &to_smoltcp,
326 &shared,
327 want_headers,
328 PEEK_BUF_SIZE,
329 PEEK_BUDGET,
330 )
331 .await?
332 } else {
333 (initial_buf, false)
334 };
335
336 if let Some(tls_state) = tls_state.clone()
337 && could_be_connect_request(&initial_buf)
338 {
339 let proxy_stream = server_rx
344 .reunite(server_tx)
345 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
346 return handle_connect_tunnel(
347 guest_dst,
348 connect_target,
349 initial_buf,
350 from_smoltcp,
351 to_smoltcp,
352 shared,
353 network_policy,
354 tls_state,
355 strict,
356 proxy_connect,
357 outbound_proxy,
358 Some(proxy_stream),
359 )
360 .await;
361 }
362
363 let mut late_connect_state = tls_state;
364 let mut secrets_handler: Option<SecretsHandler> = if is_tls {
365 None
366 } else if enforce_http_authority {
367 let host = extract_http_host(&initial_buf).unwrap_or_default();
368 Some(SecretsHandler::new_plain_http_policy(
369 &secrets,
370 &host,
371 guest_dst,
372 network_policy.clone(),
373 shared.clone(),
374 ))
375 } else if !secrets.secrets.is_empty() {
376 Some(match extract_http_host(&initial_buf) {
377 Some(host) => {
378 SecretsHandler::new_plain_http(&secrets, &host, guest_dst.ip(), &shared)
379 }
380 None => SecretsHandler::new_plain_http_invalid_host(&secrets),
381 })
382 } else {
383 None
384 };
385
386 if !initial_buf.is_empty() {
388 let out: Cow<[u8]> = match secrets_handler.as_mut() {
389 Some(h) => match h.substitute(&initial_buf) {
390 Ok(cow) => cow,
393 Err(action) => {
394 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation in first flight");
395 if matches!(action, SecretViolationAction::BlockAndTerminate) {
396 shared.trigger_termination();
397 }
398 return Ok(());
399 }
400 },
401 None => Cow::Borrowed(&initial_buf),
402 };
403 if !out.is_empty() {
404 if let Err(e) = server_tx.write_all(&out).await {
405 tracing::debug!(dst = %connect_dst, error = %e, "replay of buffered first flight failed");
406 return Ok(());
407 }
408 if let Err(e) = server_tx.flush().await {
409 tracing::debug!(dst = %connect_dst, error = %e, "flush after first flight failed");
410 return Ok(());
411 }
412 }
413 }
414
415 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
416
417 let mut guest_eof = false;
422 loop {
423 tokio::select! {
424 data = from_smoltcp.recv(), if !guest_eof => {
426 match data {
427 Some(bytes) => {
428 if let Some(tls_state) = late_connect_state.take()
429 && could_be_connect_request(&bytes)
430 {
431 let proxy_stream = server_rx
436 .reunite(server_tx)
437 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
438 return handle_connect_tunnel(
439 guest_dst,
440 connect_target,
441 bytes.to_vec(),
442 from_smoltcp,
443 to_smoltcp,
444 shared,
445 network_policy,
446 tls_state,
447 strict,
448 proxy_connect,
449 outbound_proxy,
450 Some(proxy_stream),
451 )
452 .await;
453 }
454 let out: Cow<[u8]> = match secrets_handler.as_mut() {
457 Some(h) => match h.substitute(&bytes) {
458 Ok(cow) => cow,
459 Err(action) => {
460 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation");
461 if matches!(action, SecretViolationAction::BlockAndTerminate)
462 {
463 shared.trigger_termination();
464 }
465 break;
466 }
467 },
468 None => Cow::Borrowed(&bytes),
469 };
470 if !out.is_empty() {
471 if let Err(e) = server_tx.write_all(&out).await {
472 tracing::debug!(dst = %connect_dst, error = %e, "write to server failed");
473 break;
474 }
475 if let Err(e) = server_tx.flush().await {
476 tracing::debug!(dst = %connect_dst, error = %e, "flush to server failed");
477 break;
478 }
479 }
480 }
481 None => {
486 guest_eof = true;
487 if server_tx.shutdown().await.is_err() {
488 break;
489 }
490 }
491 }
492 }
493
494 result = server_rx.read(&mut server_buf) => {
496 match result {
497 Ok(0) => break, Ok(n) => {
499 late_connect_state = None;
502 let data = Bytes::copy_from_slice(&server_buf[..n]);
503 if to_smoltcp.send(data).await.is_err() {
504 break;
506 }
507 shared.proxy_wake.wake();
510 }
511 Err(e) => {
512 tracing::debug!(dst = %connect_dst, error = %e, "read from server failed");
513 break;
514 }
515 }
516 }
517 }
518 }
519
520 Ok(())
521 }
522}
523
524#[allow(clippy::too_many_arguments)]
537pub fn spawn_tcp_proxy(
538 handle: &tokio::runtime::Handle,
539 guest_dst: SocketAddr,
540 connect_dst: SocketAddr,
541 from_smoltcp: mpsc::Receiver<Bytes>,
542 to_smoltcp: mpsc::Sender<Bytes>,
543 shared: Arc<SharedState>,
544 network_policy: Arc<NetworkPolicy>,
545 secrets: Arc<SecretsConfig>,
546 tls_state: Option<Arc<TlsState>>,
547 strict: bool,
548 proxy_connect: Arc<ProxyConnectState>,
549 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
550) {
551 let proxy = TcpProxy::new(
552 guest_dst,
553 UpstreamTcpTarget::direct(connect_dst),
554 from_smoltcp,
555 to_smoltcp,
556 shared,
557 network_policy,
558 secrets,
559 tls_state,
560 strict,
561 proxy_connect,
562 outbound_proxy,
563 );
564
565 handle.spawn(proxy.run());
566}
567
568fn strict_hostname_allow_is_opaque(
569 strict: bool,
570 network_policy: &NetworkPolicy,
571 guest_dst: SocketAddr,
572 shared: &SharedState,
573 sni: Option<&str>,
574 initial_buf: &[u8],
575) -> bool {
576 if !strict {
577 return false;
578 }
579
580 let source = if let Some(name) = sni {
581 HostnameSource::Sni(name)
582 } else if initial_buf.is_empty() || initial_buf.first() == Some(&0x16) {
583 HostnameSource::CacheOnly
584 } else {
585 return false;
586 };
587
588 network_policy.allows_egress_via_hostname(guest_dst, Protocol::Tcp, shared, source)
589}
590
591#[allow(clippy::too_many_arguments)]
597async fn handle_connect_tunnel(
598 guest_dst: SocketAddr,
599 proxy_target: UpstreamTcpTarget,
600 initial_buf: Vec<u8>,
601 mut from_smoltcp: mpsc::Receiver<Bytes>,
602 to_smoltcp: mpsc::Sender<Bytes>,
603 shared: Arc<SharedState>,
604 network_policy: Arc<NetworkPolicy>,
605 tls_state: Arc<TlsState>,
606 strict: bool,
607 proxy_connect: Arc<ProxyConnectState>,
608 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
609 preconnected_proxy: Option<TcpStream>,
610) -> io::Result<()> {
611 let proxy_dst = proxy_target.primary();
612 let connect_req =
613 parse_connect_request(buffer_connect_request(initial_buf, &mut from_smoltcp).await?)?;
614
615 let connect_headers = match sanitize_connect_headers(
616 connect_req.header_bytes(),
617 &tls_state.secrets.load(),
618 ) {
619 Ok(headers) => headers,
620 Err(action) => {
621 tracing::warn!(dst = %proxy_dst, violation = ?action, "secret violation in CONNECT headers");
622 if matches!(action, SecretViolationAction::BlockAndTerminate) {
623 shared.trigger_termination();
624 }
625 return Ok(());
626 }
627 };
628
629 let mut proxy_stream = match preconnected_proxy {
631 Some(stream) => stream,
632 None => {
633 proxy_target
634 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
635 .await?
636 }
637 };
638
639 if !connect_req.target.is_intercepted(&tls_state) {
640 let tunnel_dst = connect_req.target.guest_dst(guest_dst, &shared);
641 if strict
642 && let Some(expected_sni) = connect_req.target.expected_sni.as_deref()
643 && network_policy.allows_egress_via_hostname(
644 tunnel_dst,
645 Protocol::Tcp,
646 &shared,
647 HostnameSource::Sni(expected_sni),
648 )
649 {
650 tracing::debug!(
651 sni = %expected_sni,
652 dst = %tunnel_dst,
653 "CONNECT tunnel denied by strict hostname policy",
654 );
655 proxy_connect.mark_policy_denied();
656 shared.proxy_wake.wake();
657 return Ok(());
658 }
659 proxy_stream.write_all(&connect_headers).await?;
660 proxy_stream.flush().await?;
661 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
662 if to_smoltcp
663 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
664 .await
665 .is_err()
666 {
667 return Ok(());
668 }
669 if !proxy_resp[header_end..].is_empty()
670 && to_smoltcp
671 .send(Bytes::copy_from_slice(&proxy_resp[header_end..]))
672 .await
673 .is_err()
674 {
675 return Ok(());
676 }
677 shared.proxy_wake.wake();
678 if !connect_response_is_success(&proxy_resp[..header_end]) {
679 proxy_connect.mark_connected();
680 return Ok(());
681 }
682 if !connect_req.post_header_bytes().is_empty() {
683 proxy_stream
684 .write_all(connect_req.post_header_bytes())
685 .await?;
686 }
687 proxy_stream.flush().await?;
688 proxy_connect.mark_connected();
689 return relay_connected_stream(proxy_stream, from_smoltcp, to_smoltcp, shared).await;
690 }
691
692 proxy_stream.write_all(&connect_headers).await?;
693 proxy_stream.flush().await?;
694
695 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
696 if !connect_response_is_success(&proxy_resp[..header_end]) {
697 return Err(io::Error::new(
698 io::ErrorKind::ConnectionRefused,
699 "proxy rejected CONNECT",
700 ));
701 }
702 if !proxy_resp[header_end..].is_empty() {
703 return Err(io::Error::new(
704 io::ErrorKind::InvalidData,
705 "proxy sent unexpected bytes after CONNECT response headers",
706 ));
707 }
708 proxy_connect.mark_connected();
709
710 if to_smoltcp
711 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
712 .await
713 .is_err()
714 {
715 return Ok(());
716 }
717 shared.proxy_wake.wake();
718
719 let tls_seed = connect_req.post_header_bytes().to_vec();
720 let tls_guest_dst = connect_req.target.guest_dst(guest_dst, &shared);
721 let expected_sni = connect_req.target.expected_sni.clone();
722
723 TlsProxy::new(
724 tls_guest_dst,
725 proxy_target,
726 from_smoltcp,
727 to_smoltcp,
728 shared,
729 tls_state,
730 network_policy,
731 strict,
732 proxy_connect,
733 None,
737 )
738 .with_upstream(proxy_stream)
739 .with_expected_sni(expected_sni)
740 .with_initial_buf(tls_seed)
741 .try_run()
742 .await
743}
744
745async fn relay_connected_stream(
747 stream: TcpStream,
748 mut from_smoltcp: mpsc::Receiver<Bytes>,
749 to_smoltcp: mpsc::Sender<Bytes>,
750 shared: Arc<SharedState>,
751) -> io::Result<()> {
752 let (mut server_rx, mut server_tx) = stream.into_split();
753 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
754
755 let mut guest_eof = false;
756 loop {
757 tokio::select! {
758 data = from_smoltcp.recv(), if !guest_eof => {
759 match data {
760 Some(bytes) => {
761 server_tx.write_all(&bytes).await?;
762 server_tx.flush().await?;
763 }
764 None => {
767 guest_eof = true;
768 if server_tx.shutdown().await.is_err() {
769 break;
770 }
771 }
772 }
773 }
774 result = server_rx.read(&mut server_buf) => {
775 match result {
776 Ok(0) => break,
777 Ok(n) => {
778 if to_smoltcp
779 .send(Bytes::copy_from_slice(&server_buf[..n]))
780 .await
781 .is_err()
782 {
783 break;
784 }
785 shared.proxy_wake.wake();
786 }
787 Err(e) => return Err(e),
788 }
789 }
790 }
791 }
792
793 Ok(())
794}
795
796async fn buffer_connect_request(
797 mut buf: Vec<u8>,
798 from_smoltcp: &mut mpsc::Receiver<Bytes>,
799) -> io::Result<Vec<u8>> {
800 let timeout_fut = tokio::time::sleep(PEEK_BUDGET);
801 tokio::pin!(timeout_fut);
802
803 loop {
804 if !could_be_connect_request(&buf) {
805 return Err(io::Error::new(
806 io::ErrorKind::InvalidData,
807 "malformed CONNECT request prefix",
808 ));
809 }
810 if headers_end(&buf).is_some() {
811 return Ok(buf);
812 }
813 if buf.len() >= PEEK_BUF_SIZE {
814 return Err(io::Error::new(
815 io::ErrorKind::InvalidData,
816 "CONNECT request headers too large",
817 ));
818 }
819
820 tokio::select! {
821 biased;
822 _ = &mut timeout_fut => {
823 return Err(io::Error::new(
824 io::ErrorKind::TimedOut,
825 "timed out waiting for complete CONNECT request headers",
826 ));
827 }
828 data = from_smoltcp.recv() => match data {
829 Some(bytes) => {
830 buf.extend_from_slice(&bytes);
831 }
832 None => {
833 return Err(io::Error::new(
834 io::ErrorKind::UnexpectedEof,
835 "channel closed before complete CONNECT request headers",
836 ));
837 }
838 }
839 }
840 }
841}
842
843async fn read_connect_response_headers(stream: &mut TcpStream) -> io::Result<(Vec<u8>, usize)> {
844 tokio::time::timeout(PEEK_BUDGET, async {
845 let mut proxy_resp = Vec::with_capacity(256);
846 let mut buf = [0u8; 4096];
847 loop {
848 let n = stream.read(&mut buf).await?;
849 if n == 0 {
850 return Err(io::Error::new(
851 io::ErrorKind::UnexpectedEof,
852 "proxy closed before sending CONNECT response",
853 ));
854 }
855 proxy_resp.extend_from_slice(&buf[..n]);
856 if let Some(end) = headers_end(&proxy_resp) {
857 return Ok((proxy_resp, end));
858 }
859 if proxy_resp.len() > CONNECT_RESP_LIMIT {
860 return Err(io::Error::new(
861 io::ErrorKind::InvalidData,
862 "proxy CONNECT response too large",
863 ));
864 }
865 }
866 })
867 .await
868 .map_err(|_| {
869 io::Error::new(
870 io::ErrorKind::TimedOut,
871 "timed out waiting for proxy CONNECT response",
872 )
873 })?
874}
875
876fn sanitize_connect_headers<'a>(
877 header_bytes: &'a [u8],
878 secrets: &SecretsConfig,
879) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
880 if secrets.secrets.is_empty() {
881 return Ok(Cow::Borrowed(header_bytes));
882 }
883
884 let mut handler = SecretsHandler::new_plain_http_untrusted_metadata(secrets);
885 handler.substitute(header_bytes)
886}
887
888fn headers_end(buf: &[u8]) -> Option<usize> {
890 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
891}
892
893fn could_be_connect_request(buf: &[u8]) -> bool {
894 const PREFIX: &[u8] = b"CONNECT ";
895 if buf.is_empty() {
896 return false;
897 }
898 let n = buf.len().min(PREFIX.len());
899 buf[..n].eq_ignore_ascii_case(&PREFIX[..n])
900}
901
902fn parse_connect_request(bytes: Vec<u8>) -> io::Result<ConnectRequest> {
903 let header_end = headers_end(&bytes).ok_or_else(|| {
904 io::Error::new(
905 io::ErrorKind::InvalidData,
906 "incomplete CONNECT request headers",
907 )
908 })?;
909 let target = {
910 let request_line = bytes[..header_end]
911 .split(|&b| b == b'\n')
912 .next()
913 .unwrap_or(&[]);
914 let request_line = std::str::from_utf8(request_line)
915 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "CONNECT line is not UTF-8"))?
916 .trim_end_matches('\r');
917 let mut parts = request_line.split_ascii_whitespace();
918 let method = parts.next().unwrap_or_default();
919 let authority = parts.next().unwrap_or_default();
920 let version = parts.next().unwrap_or_default();
921 if !method.eq_ignore_ascii_case("CONNECT")
922 || authority.is_empty()
923 || !is_http_version(version)
924 || parts.next().is_some()
925 {
926 return Err(io::Error::new(
927 io::ErrorKind::InvalidData,
928 "malformed CONNECT request line",
929 ));
930 }
931 parse_connect_target(authority)?
932 };
933
934 Ok(ConnectRequest {
935 bytes,
936 header_end,
937 target,
938 })
939}
940
941fn parse_connect_target(authority: &str) -> io::Result<ConnectTarget> {
942 let authority = authority.trim();
943 let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
944 let (host, rest) = rest.split_once(']').ok_or_else(|| {
945 io::Error::new(
946 io::ErrorKind::InvalidData,
947 "malformed CONNECT IPv6 authority",
948 )
949 })?;
950 let port = rest.strip_prefix(':').ok_or_else(|| {
951 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
952 })?;
953 (host, port)
954 } else {
955 let (host, port) = authority.rsplit_once(':').ok_or_else(|| {
956 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
957 })?;
958 if host.contains(':') {
959 return Err(io::Error::new(
960 io::ErrorKind::InvalidData,
961 "CONNECT IPv6 authority must be bracketed",
962 ));
963 }
964 (host, port)
965 };
966 let host = host.trim().trim_end_matches('.');
967 if host.is_empty() {
968 return Err(io::Error::new(
969 io::ErrorKind::InvalidData,
970 "CONNECT authority missing host",
971 ));
972 }
973 let port = port
974 .parse::<u16>()
975 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid CONNECT port"))?;
976 let expected_sni = host
977 .parse::<IpAddr>()
978 .is_err()
979 .then(|| host.to_ascii_lowercase());
980
981 Ok(ConnectTarget {
982 host: host.to_ascii_lowercase(),
983 port,
984 expected_sni,
985 })
986}
987
988fn is_http_version(version: &str) -> bool {
989 let Some(version) = version.strip_prefix("HTTP/") else {
990 return false;
991 };
992 let Some((major, minor)) = version.split_once('.') else {
993 return false;
994 };
995 !major.is_empty()
996 && !minor.is_empty()
997 && major.bytes().all(|b| b.is_ascii_digit())
998 && minor.bytes().all(|b| b.is_ascii_digit())
999}
1000
1001fn connect_response_is_success(headers: &[u8]) -> bool {
1002 let Some(status_line) = headers.split(|&b| b == b'\n').next() else {
1003 return false;
1004 };
1005 let Ok(status_line) = std::str::from_utf8(status_line) else {
1006 return false;
1007 };
1008 let mut parts = status_line.trim_end_matches('\r').split_ascii_whitespace();
1009 let version = parts.next().unwrap_or_default();
1010 let status = parts.next().unwrap_or_default();
1011 is_http_version(version)
1012 && status.len() == 3
1013 && status
1014 .parse::<u16>()
1015 .is_ok_and(|code| (200..300).contains(&code))
1016}
1017
1018fn extract_http_host(buf: &[u8]) -> Option<String> {
1028 if buf.first() == Some(&0x16) {
1029 return None;
1030 }
1031 let mut headers = vec![httparse::EMPTY_HEADER; (buf.len() / 4).max(16)];
1037 let mut req = httparse::Request::new(&mut headers);
1038 req.parse(buf).ok()?;
1039 req.headers
1040 .iter()
1041 .find(|h| h.name.eq_ignore_ascii_case("host"))
1042 .and_then(|h| std::str::from_utf8(h.value).ok())
1043 .map(|v| {
1044 let host = v.trim();
1045 host.rsplit_once(':')
1047 .map(|(h, _)| h)
1048 .unwrap_or(host)
1049 .to_ascii_lowercase()
1050 })
1051 .filter(|h| !h.is_empty())
1052}
1053
1054#[allow(clippy::too_many_arguments)]
1071async fn classify_first_flight(
1072 mut buf: Vec<u8>,
1073 from_smoltcp: &mut mpsc::Receiver<Bytes>,
1074 server_rx: &mut tokio::net::tcp::OwnedReadHalf,
1075 to_smoltcp: &mpsc::Sender<Bytes>,
1076 shared: &SharedState,
1077 want_headers: bool,
1078 max: usize,
1079 budget: Duration,
1080) -> io::Result<(Vec<u8>, bool)> {
1081 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
1082 let timeout_fut = tokio::time::sleep(budget);
1083 tokio::pin!(timeout_fut);
1084
1085 loop {
1086 if !buf.is_empty() {
1092 let is_tls = buf.first() == Some(&0x16);
1093 let not_http = !is_tls
1094 && (!looks_like_http_request_prefix(&buf) || first_line_is_not_http_request(&buf));
1095 let done = !want_headers
1096 || is_tls
1097 || not_http
1098 || buf.len() >= max
1099 || buf.windows(4).any(|w| w == b"\r\n\r\n");
1100 if done {
1101 return Ok((buf, is_tls));
1102 }
1103 }
1104
1105 tokio::select! {
1106 biased;
1107 _ = &mut timeout_fut => {
1108 let is_tls = buf.first() == Some(&0x16);
1109 return Ok((buf, is_tls));
1110 }
1111 guest = from_smoltcp.recv() => match guest {
1114 Some(bytes) => buf.extend_from_slice(&bytes),
1115 None => {
1116 let is_tls = buf.first() == Some(&0x16);
1117 return Ok((buf, is_tls));
1118 }
1119 },
1120 server = server_rx.read(&mut server_buf) => match server {
1123 Ok(0) => {
1124 let is_tls = buf.first() == Some(&0x16);
1125 return Ok((buf, is_tls));
1126 }
1127 Ok(n) => {
1128 let data = Bytes::copy_from_slice(&server_buf[..n]);
1129 if to_smoltcp.send(data).await.is_err() {
1130 let is_tls = buf.first() == Some(&0x16);
1131 return Ok((buf, is_tls));
1132 }
1133 shared.proxy_wake.wake();
1134 }
1135 Err(e) => return Err(e),
1136 },
1137 }
1138 }
1139}
1140
1141async fn peek_for_sni(
1151 rx: &mut mpsc::Receiver<Bytes>,
1152 max: usize,
1153 budget: Duration,
1154) -> (Vec<u8>, Option<String>) {
1155 let mut buf = Vec::with_capacity(PEEK_BUF_SIZE.min(8192));
1156 let timeout_fut = tokio::time::sleep(budget);
1157 tokio::pin!(timeout_fut);
1158
1159 let raw_sni = loop {
1160 tokio::select! {
1161 biased;
1162 _ = &mut timeout_fut => break None,
1163 data = rx.recv() => {
1164 match data {
1165 Some(bytes) => {
1166 buf.extend_from_slice(&bytes);
1167 if buf.first() != Some(&0x16) {
1172 break None;
1173 }
1174 if let Some(name) = sni::extract_sni(&buf) {
1175 break Some(name);
1176 }
1177 if buf.len() >= max {
1178 break None;
1179 }
1180 }
1181 None => break None,
1182 }
1183 }
1184 }
1185 };
1186
1187 let canonical = raw_sni.map(|s| s.trim_end_matches('.').to_ascii_lowercase());
1188 (buf, canonical)
1189}
1190
1191#[cfg(test)]
1196mod tests {
1197 use super::*;
1198
1199 fn synthetic_client_hello(sni: &str) -> Vec<u8> {
1203 let host_bytes = sni.as_bytes();
1206 let host_len = host_bytes.len() as u16;
1207 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();
1212 body.extend_from_slice(&[0x03, 0x03]);
1214 body.extend_from_slice(&[0u8; 32]);
1216 body.push(0);
1218 body.extend_from_slice(&[0x00, 0x02, 0x00, 0x2f]);
1220 body.extend_from_slice(&[0x01, 0x00]);
1222 body.extend_from_slice(&extensions_total.to_be_bytes());
1224 body.extend_from_slice(&[0x00, 0x00]);
1226 body.extend_from_slice(&extension_data_len.to_be_bytes());
1227 body.extend_from_slice(&server_name_list_len.to_be_bytes());
1228 body.push(0x00); body.extend_from_slice(&host_len.to_be_bytes());
1230 body.extend_from_slice(host_bytes);
1231
1232 let handshake_len = body.len() as u32;
1233 let mut hs = Vec::new();
1234 hs.push(0x01); hs.extend_from_slice(&handshake_len.to_be_bytes()[1..]); hs.extend_from_slice(&body);
1237
1238 let record_len = hs.len() as u16;
1239 let mut record = Vec::new();
1240 record.extend_from_slice(&[0x16, 0x03, 0x01]); record.extend_from_slice(&record_len.to_be_bytes());
1242 record.extend_from_slice(&hs);
1243
1244 record
1245 }
1246
1247 #[tokio::test]
1248 async fn connect_upstream_dials_target_directly_without_outbound_proxy() {
1249 use tokio::net::TcpListener;
1250
1251 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1252 let addr = listener.local_addr().unwrap();
1253 let accept = tokio::spawn(async move {
1254 let (mut sock, _) = listener.accept().await.unwrap();
1255 let mut buf = [0u8; 5];
1256 sock.read_exact(&mut buf).await.unwrap();
1257 assert_eq!(&buf, b"hello");
1258 });
1259
1260 let shared = SharedState::new(4);
1261 let proxy_connect = ProxyConnectState::new();
1262 let mut stream = UpstreamTcpTarget::direct(addr)
1263 .connect(&proxy_connect, &shared, None)
1264 .await
1265 .unwrap();
1266 stream.write_all(b"hello").await.unwrap();
1267
1268 accept.await.unwrap();
1269 assert!(matches!(
1270 proxy_connect.status(),
1271 ProxyConnectStatus::Connected
1272 ));
1273 }
1274
1275 #[tokio::test]
1276 async fn early_http_connect_dials_proxy_through_configured_socks5_proxy() {
1277 let _ = rustls::crypto::ring::default_provider().install_default();
1278
1279 let http_proxy_addr: SocketAddr = "93.184.216.34:3128".parse().unwrap();
1282 let socks_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1283 let outbound_proxy = ResolvedOutboundProxy::Socks5 {
1284 address: socks_listener.local_addr().unwrap(),
1285 credentials: None,
1286 };
1287 let socks_task = tokio::spawn(async move {
1288 let (mut client, _) = socks_listener.accept().await.unwrap();
1289
1290 let mut greeting = [0u8; 3];
1291 client.read_exact(&mut greeting).await.unwrap();
1292 assert_eq!(greeting, [0x05, 0x01, 0x00]);
1293 client.write_all(&[0x05, 0x00]).await.unwrap();
1294
1295 let mut socks_request = [0u8; 10];
1296 client.read_exact(&mut socks_request).await.unwrap();
1297 assert_eq!(socks_request[0..4], [0x05, 0x01, 0x00, 0x01]);
1298 assert_eq!(&socks_request[4..8], &[93, 184, 216, 34]);
1299 assert_eq!(
1300 u16::from_be_bytes([socks_request[8], socks_request[9]]),
1301 3128
1302 );
1303 client
1304 .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1305 .await
1306 .unwrap();
1307
1308 let expected_connect =
1309 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
1310 let mut connect_request = vec![0u8; expected_connect.len()];
1311 client.read_exact(&mut connect_request).await.unwrap();
1312 assert_eq!(&connect_request, expected_connect);
1313 client
1314 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
1315 .await
1316 .unwrap();
1317 });
1318
1319 let connect_request =
1320 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n".to_vec();
1321 let (from_tx, from_rx) = mpsc::channel(1);
1322 let (to_tx, mut to_rx) = mpsc::channel(1);
1323 drop(from_tx);
1324
1325 let tls_state = Arc::new(
1326 TlsState::new(
1327 microsandbox_types::TlsConfig::default(),
1328 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1329 )
1330 .unwrap(),
1331 );
1332 let proxy_connect = Arc::new(ProxyConnectState::new());
1333
1334 handle_connect_tunnel(
1335 http_proxy_addr,
1336 UpstreamTcpTarget::direct(http_proxy_addr),
1337 connect_request,
1338 from_rx,
1339 to_tx,
1340 Arc::new(SharedState::new(4)),
1341 Arc::new(NetworkPolicy::default()),
1342 tls_state,
1343 false,
1344 proxy_connect.clone(),
1345 Some(Arc::new(outbound_proxy)),
1346 None,
1347 )
1348 .await
1349 .unwrap();
1350
1351 let response = to_rx.recv().await.unwrap();
1352 assert_eq!(
1353 &response[..],
1354 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1355 );
1356 socks_task.await.unwrap();
1357 assert!(matches!(
1358 proxy_connect.status(),
1359 ProxyConnectStatus::Connected
1360 ));
1361 }
1362
1363 #[test]
1364 fn could_be_connect_request_matches_split_prefixes_only() {
1365 assert!(could_be_connect_request(b"C"));
1366 assert!(could_be_connect_request(b"connect "));
1367 assert!(could_be_connect_request(b"CONNECT example.com:443"));
1368 assert!(!could_be_connect_request(b"CLIENT"));
1369 assert!(!could_be_connect_request(b"GET / HTTP/1.1\r\n"));
1370 }
1371
1372 #[tokio::test]
1373 async fn buffer_connect_request_reads_split_headers() {
1374 let (tx, mut rx) = mpsc::channel(4);
1375 tx.send(Bytes::from_static(b"NECT example.com:443 HTTP/1.1\r\n"))
1376 .await
1377 .unwrap();
1378 tx.send(Bytes::from_static(b"Host: example.com\r\n\r\n"))
1379 .await
1380 .unwrap();
1381 drop(tx);
1382
1383 let buffered = buffer_connect_request(b"CON".to_vec(), &mut rx)
1384 .await
1385 .unwrap();
1386 let parsed = parse_connect_request(buffered).unwrap();
1387
1388 assert_eq!(parsed.target.host, "example.com");
1389 assert_eq!(parsed.target.port, 443);
1390 assert_eq!(parsed.target.expected_sni.as_deref(), Some("example.com"));
1391 assert!(parsed.post_header_bytes().is_empty());
1392 }
1393
1394 #[test]
1395 fn parse_connect_request_preserves_post_header_tls_seed() {
1396 let mut request = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
1397 request.extend_from_slice(b"\x16\x03\x01client-hello");
1398
1399 let parsed = parse_connect_request(request).unwrap();
1400
1401 assert_eq!(
1402 parsed.header_bytes(),
1403 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n"
1404 );
1405 assert_eq!(parsed.post_header_bytes(), b"\x16\x03\x01client-hello");
1406 }
1407
1408 #[test]
1409 fn parse_connect_target_requires_authority_port() {
1410 assert!(parse_connect_target("example.com").is_err());
1411 assert!(parse_connect_target("2001:db8::1:443").is_err());
1412
1413 let target = parse_connect_target("[2001:db8::1]:8443").unwrap();
1414 assert_eq!(target.host, "2001:db8::1");
1415 assert_eq!(target.port, 8443);
1416 assert_eq!(target.expected_sni, None);
1417 }
1418
1419 #[test]
1420 fn connect_response_success_requires_exact_2xx_status_code() {
1421 assert!(connect_response_is_success(
1422 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1423 ));
1424 assert!(connect_response_is_success(
1425 b"HTTP/1.1 204 Connection Established\r\n\r\n"
1426 ));
1427 assert!(!connect_response_is_success(b"HTTP/1.1 2000 Weird\r\n\r\n"));
1428 assert!(!connect_response_is_success(b"HTTP/1.1 199 Nope\r\n\r\n"));
1429 assert!(!connect_response_is_success(b"NOTHTTP 200 OK\r\n\r\n"));
1430 }
1431
1432 #[tokio::test]
1433 async fn peek_for_sni_extracts_and_canonicalizes() {
1434 let (tx, mut rx) = mpsc::channel(4);
1435 let hello = synthetic_client_hello("Example.COM");
1436 tx.send(Bytes::from(hello.clone())).await.unwrap();
1437 drop(tx); let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1440 assert_eq!(sni.as_deref(), Some("example.com"));
1441 assert_eq!(buf, hello);
1442 }
1443
1444 #[tokio::test]
1445 async fn peek_for_sni_returns_none_on_channel_close_without_data() {
1446 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1447 drop(tx);
1448 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1449 assert!(buf.is_empty());
1450 assert_eq!(sni, None);
1451 }
1452
1453 #[tokio::test]
1454 async fn peek_for_sni_returns_none_on_non_tls_data() {
1455 let (tx, mut rx) = mpsc::channel(4);
1456 tx.send(Bytes::from_static(
1458 b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
1459 ))
1460 .await
1461 .unwrap();
1462 drop(tx);
1463 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1464 assert!(
1465 !buf.is_empty(),
1466 "buffered bytes must be returned for replay"
1467 );
1468 assert_eq!(sni, None);
1469 }
1470
1471 #[tokio::test]
1472 async fn peek_for_sni_falls_back_on_timeout() {
1473 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1474 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, Duration::from_millis(50)).await;
1476 drop(tx);
1477 assert!(buf.is_empty());
1478 assert_eq!(sni, None);
1479 }
1480
1481 #[tokio::test]
1482 async fn peek_for_sni_caps_at_max_bytes() {
1483 let (tx, mut rx) = mpsc::channel(4);
1484 let mut first = vec![0u8; 8192];
1488 first[0] = 0x16;
1489 tx.send(Bytes::from(first)).await.unwrap();
1490 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1491 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1492 drop(tx);
1493
1494 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1495 assert_eq!(sni, None, "no SNI in non-TLS data");
1496 assert!(
1497 buf.len() >= PEEK_BUF_SIZE,
1498 "buffer must hit the cap before bail-out: got {}",
1499 buf.len()
1500 );
1501 }
1502
1503 #[tokio::test]
1504 async fn peek_for_sni_bails_immediately_on_non_tls_first_byte() {
1505 let (tx, mut rx) = mpsc::channel(4);
1506 tx.send(Bytes::from_static(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
1508 .await
1509 .unwrap();
1510 drop(tx);
1511
1512 let started = std::time::Instant::now();
1515 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1516 let elapsed = started.elapsed();
1517 assert_eq!(sni, None);
1518 assert!(buf.starts_with(b"GET"));
1519 assert!(
1520 elapsed < Duration::from_millis(500),
1521 "non-TLS bail must be fast: took {elapsed:?}"
1522 );
1523 }
1524
1525 use std::net::IpAddr;
1530 use std::time::Duration as StdDuration;
1531
1532 use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
1533 use crate::policy::{Action, Destination, NetworkPolicy, PortRange, Rule};
1534
1535 const SHARED_FASTLY_IP: &str = "151.101.0.223";
1536
1537 fn shared_with(host: &str, ip: &str) -> SharedState {
1538 let shared = SharedState::new(4);
1539 shared.cache_resolved_hostname(
1540 host,
1541 ResolvedHostnameFamily::Ipv4,
1542 [ip.parse::<IpAddr>().unwrap()],
1543 StdDuration::from_secs(60),
1544 );
1545 shared
1546 }
1547
1548 fn allow_https(domain: &str) -> Rule {
1549 Rule {
1550 direction: crate::policy::Direction::Egress,
1551 destination: Destination::Domain(domain.parse().unwrap()),
1552 protocols: vec![Protocol::Tcp],
1553 ports: vec![PortRange::single(443)],
1554 action: Action::Allow,
1555 }
1556 }
1557
1558 fn allow_tcp(domain: &str, port: u16) -> Rule {
1559 Rule {
1560 direction: crate::policy::Direction::Egress,
1561 destination: Destination::Domain(domain.parse().unwrap()),
1562 protocols: vec![Protocol::Tcp],
1563 ports: vec![PortRange::single(port)],
1564 action: Action::Allow,
1565 }
1566 }
1567
1568 #[tokio::test]
1571 async fn integration_sni_overrides_cache_for_over_allow() {
1572 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1573 let policy = NetworkPolicy {
1574 default_egress: Action::Deny,
1575 default_ingress: Action::Allow,
1576 rules: vec![allow_https("pypi.org")],
1577 };
1578 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1579
1580 let (tx, mut rx) = mpsc::channel(4);
1581 tx.send(Bytes::from(synthetic_client_hello("evil.com")))
1582 .await
1583 .unwrap();
1584 drop(tx);
1585
1586 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1587 assert_eq!(sni.as_deref(), Some("evil.com"));
1588 assert!(!initial_buf.is_empty());
1589
1590 let source = sni
1591 .as_deref()
1592 .map(HostnameSource::Sni)
1593 .unwrap_or(HostnameSource::CacheOnly);
1594 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1595 assert_eq!(
1596 eval,
1597 EgressEvaluation::Deny,
1598 "SNI=evil.com must not piggy-back on the cached pypi.org match",
1599 );
1600 }
1601
1602 #[tokio::test]
1605 async fn integration_sni_overrides_cache_for_over_block() {
1606 let shared = shared_with("ads.example.com", SHARED_FASTLY_IP);
1607 let policy = NetworkPolicy {
1608 default_egress: Action::Allow,
1609 default_ingress: Action::Allow,
1610 rules: vec![Rule::deny_egress(Destination::Domain(
1611 "ads.example.com".parse().unwrap(),
1612 ))],
1613 };
1614 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1615
1616 let (tx, mut rx) = mpsc::channel(4);
1617 tx.send(Bytes::from(synthetic_client_hello("api.example.com")))
1618 .await
1619 .unwrap();
1620 drop(tx);
1621
1622 let (_initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1623 assert_eq!(sni.as_deref(), Some("api.example.com"));
1624
1625 let source = sni
1626 .as_deref()
1627 .map(HostnameSource::Sni)
1628 .unwrap_or(HostnameSource::CacheOnly);
1629 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1630 assert_eq!(
1631 eval,
1632 EgressEvaluation::Allow,
1633 "SNI=api.example.com must not be caught by the deny on ads.example.com",
1634 );
1635 }
1636
1637 #[tokio::test]
1640 async fn integration_non_tls_falls_back_to_cache() {
1641 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1642 let policy = NetworkPolicy {
1643 default_egress: Action::Deny,
1644 default_ingress: Action::Allow,
1645 rules: vec![allow_https("pypi.org")],
1646 };
1647 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1648
1649 let (tx, mut rx) = mpsc::channel(4);
1650 tx.send(Bytes::from_static(
1652 b"GET / HTTP/1.1\r\nHost: pypi.org\r\n\r\n",
1653 ))
1654 .await
1655 .unwrap();
1656 drop(tx);
1657
1658 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1659 assert_eq!(sni, None, "non-TLS data → no SNI");
1660 assert!(
1661 !initial_buf.is_empty(),
1662 "buffered bytes must survive for replay"
1663 );
1664
1665 let source = sni
1666 .as_deref()
1667 .map(HostnameSource::Sni)
1668 .unwrap_or(HostnameSource::CacheOnly);
1669 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1670 assert_eq!(
1671 eval,
1672 EgressEvaluation::Allow,
1673 "cache-only fallback must still allow the cached hostname's IP",
1674 );
1675 }
1676
1677 #[tokio::test]
1680 async fn integration_sni_matches_domain_suffix_with_cache_binding() {
1681 let shared = shared_with("files.pythonhosted.org", SHARED_FASTLY_IP);
1682 let policy = NetworkPolicy {
1683 default_egress: Action::Deny,
1684 default_ingress: Action::Allow,
1685 rules: vec![Rule {
1686 direction: crate::policy::Direction::Egress,
1687 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1688 protocols: vec![Protocol::Tcp],
1689 ports: vec![PortRange::single(443)],
1690 action: Action::Allow,
1691 }],
1692 };
1693 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1694
1695 let (tx, mut rx) = mpsc::channel(4);
1696 tx.send(Bytes::from(synthetic_client_hello(
1697 "files.pythonhosted.org",
1698 )))
1699 .await
1700 .unwrap();
1701 drop(tx);
1702
1703 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1704 let source = sni
1705 .as_deref()
1706 .map(HostnameSource::Sni)
1707 .unwrap_or(HostnameSource::CacheOnly);
1708 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1709 assert_eq!(eval, EgressEvaluation::Allow);
1710 }
1711
1712 #[tokio::test]
1717 async fn integration_sni_denies_domain_suffix_without_cache_binding() {
1718 let shared = SharedState::new(4); let policy = NetworkPolicy {
1720 default_egress: Action::Deny,
1721 default_ingress: Action::Allow,
1722 rules: vec![Rule {
1723 direction: crate::policy::Direction::Egress,
1724 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1725 protocols: vec![Protocol::Tcp],
1726 ports: vec![PortRange::single(443)],
1727 action: Action::Allow,
1728 }],
1729 };
1730 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1731
1732 let (tx, mut rx) = mpsc::channel(4);
1733 tx.send(Bytes::from(synthetic_client_hello(
1734 "files.pythonhosted.org",
1735 )))
1736 .await
1737 .unwrap();
1738 drop(tx);
1739
1740 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1741 let source = sni
1742 .as_deref()
1743 .map(HostnameSource::Sni)
1744 .unwrap_or(HostnameSource::CacheOnly);
1745 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1746 assert_eq!(eval, EgressEvaluation::Deny);
1747 }
1748
1749 #[test]
1752 fn extract_http_host_basic() {
1753 let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1754 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1755 }
1756
1757 #[test]
1758 fn extract_http_host_strips_port() {
1759 let buf = b"POST /api HTTP/1.1\r\nHost: api.company.com:8080\r\n\r\n";
1760 assert_eq!(extract_http_host(buf), Some("api.company.com".into()));
1761 }
1762
1763 #[test]
1764 fn extract_http_host_case_insensitive_lowercased() {
1765 let buf = b"GET / HTTP/1.1\r\nhost: Example.COM\r\n\r\n";
1766 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1767 }
1768
1769 #[test]
1770 fn extract_http_host_no_host_header() {
1771 let buf = b"GET / HTTP/1.1\r\nX-Other: foo\r\n\r\n";
1772 assert_eq!(extract_http_host(buf), None);
1773 }
1774
1775 #[test]
1776 fn extract_http_host_incomplete_headers() {
1777 let buf = b"GET / HTTP/1.1\r\nHost: x";
1778 assert_eq!(extract_http_host(buf), None);
1779 }
1780
1781 #[test]
1782 fn extract_http_host_tls_first_byte() {
1783 let buf = [0x16u8, 0x03, 0x01, 0x00, 0x01];
1784 assert_eq!(extract_http_host(&buf), None);
1785 }
1786
1787 #[test]
1788 fn extract_http_host_with_many_headers() {
1789 let mut req = Vec::from(&b"GET / HTTP/1.1\r\n"[..]);
1792 for i in 0..100 {
1793 req.extend_from_slice(format!("X-Pad-{i}: v\r\n").as_bytes());
1794 }
1795 req.extend_from_slice(b"Host: example.com\r\n\r\n");
1796 assert_eq!(extract_http_host(&req), Some("example.com".into()));
1797 }
1798
1799 use std::sync::Arc;
1802 use tokio::io::AsyncReadExt;
1803 use tokio::net::TcpListener;
1804 use tokio::task::JoinHandle;
1805
1806 use crate::secrets::config::{
1807 HostPattern, SecretEntry, SecretSubstitution, SecretViolationAction, SecretsConfig,
1808 };
1809
1810 fn make_plain_http_secret(placeholder: &str, value: &str, require_tls: bool) -> SecretsConfig {
1811 SecretsConfig {
1812 secrets: vec![SecretEntry {
1813 env_var: "API_KEY".into(),
1814 value: zeroize::Zeroizing::new(value.into()),
1815 source: None,
1816 placeholder: placeholder.into(),
1817 allowed_hosts: vec![HostPattern::Any],
1818 substitution: SecretSubstitution {
1819 headers: true,
1820 query: false,
1821 body: false,
1822 },
1823 passthrough_hosts: Vec::new(),
1824 violation_action: None,
1825 require_tls_identity: require_tls,
1826 }],
1827 ..Default::default()
1828 }
1829 }
1830
1831 fn make_host_bound_secret(placeholder: &str, value: &str, host: &str) -> SecretsConfig {
1832 SecretsConfig {
1833 secrets: vec![SecretEntry {
1834 env_var: "API_KEY".into(),
1835 value: zeroize::Zeroizing::new(value.into()),
1836 source: None,
1837 placeholder: placeholder.into(),
1838 allowed_hosts: vec![HostPattern::Exact(host.into())],
1839 substitution: SecretSubstitution::default(),
1840 passthrough_hosts: Vec::new(),
1841 violation_action: None,
1842 require_tls_identity: true,
1843 }],
1844 ..Default::default()
1845 }
1846 }
1847
1848 #[test]
1849 fn sanitize_connect_headers_blocks_placeholder_metadata_header_by_default() {
1850 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1851 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";
1852
1853 assert_eq!(
1854 sanitize_connect_headers(headers, &secrets),
1855 Err(SecretViolationAction::BlockAndLog)
1856 );
1857 }
1858
1859 #[test]
1860 fn sanitize_connect_headers_respects_block_and_terminate() {
1861 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1862 secrets.violation_action = SecretViolationAction::BlockAndTerminate;
1863 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";
1864
1865 assert_eq!(
1866 sanitize_connect_headers(headers, &secrets),
1867 Err(SecretViolationAction::BlockAndTerminate)
1868 );
1869 }
1870
1871 #[test]
1872 fn sanitize_connect_headers_respects_explicit_passthrough() {
1873 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1874 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
1875 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";
1876
1877 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1878
1879 assert_eq!(sanitized.as_ref(), headers);
1880 assert!(
1881 !String::from_utf8_lossy(sanitized.as_ref()).contains("real-secret-value"),
1882 "passthrough must never substitute real secrets into CONNECT metadata"
1883 );
1884 }
1885
1886 #[test]
1887 fn sanitize_connect_headers_keeps_safe_metadata_headers() {
1888 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1889 let headers =
1890 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nUser-Agent: curl\r\n\r\n";
1891
1892 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1893
1894 assert_eq!(sanitized.as_ref(), headers);
1895 }
1896
1897 #[test]
1898 fn sanitize_connect_headers_blocks_placeholder_in_request_line() {
1899 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1900 let headers = b"CONNECT $MSB_KEY:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
1901
1902 assert_eq!(
1903 sanitize_connect_headers(headers, &secrets),
1904 Err(SecretViolationAction::BlockAndLog)
1905 );
1906 }
1907
1908 async fn spawn_sink() -> (SocketAddr, JoinHandle<Vec<u8>>) {
1909 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1910 let addr = listener.local_addr().unwrap();
1911 let handle = tokio::spawn(async move {
1912 let (mut stream, _) = listener.accept().await.unwrap();
1913 let mut received = Vec::new();
1914 let mut buf = vec![0u8; 4096];
1915 loop {
1916 match stream.read(&mut buf).await {
1917 Ok(0) | Err(_) => break,
1918 Ok(n) => received.extend_from_slice(&buf[..n]),
1919 }
1920 }
1921 received
1922 });
1923 (addr, handle)
1924 }
1925
1926 async fn assert_server_first_banner_is_immediate(
1927 policy: NetworkPolicy,
1928 tls_state: Option<Arc<TlsState>>,
1929 ) {
1930 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1931 let addr = listener.local_addr().unwrap();
1932 let server = tokio::spawn(async move {
1933 let (mut stream, _) = listener.accept().await.unwrap();
1934 stream.write_all(b"READY\n").await.unwrap();
1935 let mut received = Vec::new();
1936 stream.read_to_end(&mut received).await.unwrap();
1937 });
1938
1939 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1940 let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
1941 spawn_tcp_proxy(
1942 &tokio::runtime::Handle::current(),
1943 addr,
1944 addr,
1945 from_rx,
1946 to_tx,
1947 Arc::new(SharedState::new(4)),
1948 Arc::new(policy),
1949 Arc::new(SecretsConfig::default()),
1950 tls_state,
1951 false,
1952 Arc::new(ProxyConnectState::new()),
1953 None,
1954 );
1955
1956 let banner = tokio::time::timeout(Duration::from_secs(1), to_rx.recv())
1957 .await
1958 .expect("server-first banner was delayed by a pre-connect peek")
1959 .expect("proxy closed before relaying the server-first banner");
1960 assert_eq!(banner, b"READY\n"[..]);
1961
1962 drop(from_tx);
1963 tokio::time::timeout(Duration::from_secs(7), server)
1964 .await
1965 .expect("proxy did not close the upstream connection")
1966 .unwrap();
1967 }
1968
1969 #[tokio::test]
1970 async fn server_first_connection_skips_unrelated_domain_policy_peek() {
1971 let policy = NetworkPolicy {
1972 default_egress: Action::Allow,
1973 default_ingress: Action::Allow,
1974 rules: vec![allow_https("unused.example")],
1975 };
1976
1977 assert_server_first_banner_is_immediate(policy, None).await;
1978 }
1979
1980 #[tokio::test]
1981 async fn server_first_connection_skips_eager_connect_peek() {
1982 let _ = rustls::crypto::ring::default_provider().install_default();
1983 let tls_state = Arc::new(
1984 TlsState::new(
1985 microsandbox_types::TlsConfig::default(),
1986 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1987 )
1988 .unwrap(),
1989 );
1990
1991 assert_server_first_banner_is_immediate(NetworkPolicy::default(), Some(tls_state)).await;
1992 }
1993
1994 async fn relay_through_proxy(
1995 request: Vec<u8>,
1996 secrets: SecretsConfig,
1997 handle: JoinHandle<Vec<u8>>,
1998 server_addr: SocketAddr,
1999 ) -> Vec<u8> {
2000 relay_through_proxy_with_policy(
2001 request,
2002 Arc::new(SharedState::new(4)),
2003 Arc::new(NetworkPolicy::default()),
2004 secrets,
2005 handle,
2006 server_addr,
2007 )
2008 .await
2009 }
2010
2011 async fn relay_through_proxy_with_policy(
2012 request: Vec<u8>,
2013 shared: Arc<SharedState>,
2014 policy: Arc<NetworkPolicy>,
2015 secrets: SecretsConfig,
2016 handle: JoinHandle<Vec<u8>>,
2017 server_addr: SocketAddr,
2018 ) -> Vec<u8> {
2019 relay_chunks_through_proxy_with_policy(
2020 vec![request],
2021 shared,
2022 policy,
2023 secrets,
2024 handle,
2025 server_addr,
2026 )
2027 .await
2028 }
2029
2030 async fn relay_chunks_through_proxy_with_policy(
2031 chunks: Vec<Vec<u8>>,
2032 shared: Arc<SharedState>,
2033 policy: Arc<NetworkPolicy>,
2034 secrets: SecretsConfig,
2035 handle: JoinHandle<Vec<u8>>,
2036 server_addr: SocketAddr,
2037 ) -> Vec<u8> {
2038 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2039 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2040 let secrets = Arc::new(secrets);
2041 let proxy_connect = Arc::new(ProxyConnectState::new());
2042
2043 for chunk in chunks {
2044 from_tx.send(Bytes::from(chunk)).await.unwrap();
2045 }
2046 drop(from_tx);
2047
2048 TcpProxy::new(
2049 server_addr,
2050 UpstreamTcpTarget::direct(server_addr),
2051 from_rx,
2052 to_tx,
2053 shared,
2054 policy,
2055 secrets,
2056 None,
2057 false,
2058 proxy_connect,
2059 None,
2060 )
2061 .try_run()
2062 .await
2063 .unwrap();
2064
2065 handle.await.unwrap()
2066 }
2067
2068 #[tokio::test]
2069 async fn plain_http_domain_policy_allows_matching_host() {
2070 let (addr, sink) = spawn_sink().await;
2071 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2072 let policy = Arc::new(NetworkPolicy {
2073 default_egress: Action::Deny,
2074 default_ingress: Action::Allow,
2075 rules: vec![allow_tcp("allowed.example", addr.port())],
2076 });
2077
2078 let wire = relay_through_proxy_with_policy(
2079 b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2080 shared,
2081 policy,
2082 SecretsConfig::default(),
2083 sink,
2084 addr,
2085 )
2086 .await;
2087
2088 assert_eq!(wire, b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2089 }
2090
2091 #[tokio::test]
2092 async fn plain_http_domain_policy_blocks_host_switch() {
2093 let (addr, sink) = spawn_sink().await;
2094 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2095 let policy = Arc::new(NetworkPolicy {
2096 default_egress: Action::Deny,
2097 default_ingress: Action::Allow,
2098 rules: vec![allow_tcp("allowed.example", addr.port())],
2099 });
2100
2101 let wire = relay_through_proxy_with_policy(
2102 b"GET / HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2103 shared,
2104 policy,
2105 SecretsConfig::default(),
2106 sink,
2107 addr,
2108 )
2109 .await;
2110
2111 assert!(
2112 wire.is_empty(),
2113 "switched HTTP authority must not reach upstream, got: {wire:?}"
2114 );
2115 }
2116
2117 #[tokio::test]
2118 async fn plain_http_domain_policy_blocks_keep_alive_host_switch() {
2119 let (addr, sink) = spawn_sink().await;
2120 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2121 let policy = Arc::new(NetworkPolicy {
2122 default_egress: Action::Deny,
2123 default_ingress: Action::Allow,
2124 rules: vec![allow_tcp("allowed.example", addr.port())],
2125 });
2126
2127 let wire = relay_chunks_through_proxy_with_policy(
2128 vec![
2129 b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2130 b"GET /two HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2131 ],
2132 shared,
2133 policy,
2134 SecretsConfig::default(),
2135 sink,
2136 addr,
2137 )
2138 .await;
2139
2140 assert_eq!(wire, b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2141 }
2142
2143 #[test]
2144 fn strict_hostname_allow_blocks_sni_authority_before_tcp_dial() {
2145 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2146 let shared = shared_with("allowed.example", "127.0.0.1");
2147 let policy = NetworkPolicy {
2148 default_egress: Action::Deny,
2149 default_ingress: Action::Allow,
2150 rules: vec![allow_tcp("allowed.example", dst.port())],
2151 };
2152
2153 assert!(strict_hostname_allow_is_opaque(
2154 true,
2155 &policy,
2156 dst,
2157 &shared,
2158 Some("allowed.example"),
2159 &synthetic_client_hello("allowed.example"),
2160 ));
2161 }
2162
2163 #[test]
2164 fn strict_hostname_allow_blocks_tls_without_sni_before_tcp_dial() {
2165 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2166 let shared = shared_with("allowed.example", "127.0.0.1");
2167 let policy = NetworkPolicy {
2168 default_egress: Action::Deny,
2169 default_ingress: Action::Allow,
2170 rules: vec![allow_tcp("allowed.example", dst.port())],
2171 };
2172
2173 assert!(strict_hostname_allow_is_opaque(
2174 true,
2175 &policy,
2176 dst,
2177 &shared,
2178 None,
2179 &[0x16, 0x03, 0x01],
2180 ));
2181 }
2182
2183 #[test]
2184 fn strict_hostname_allow_leaves_plain_http_for_authority_validation() {
2185 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 80);
2186 let shared = shared_with("allowed.example", "127.0.0.1");
2187 let policy = NetworkPolicy {
2188 default_egress: Action::Deny,
2189 default_ingress: Action::Allow,
2190 rules: vec![allow_tcp("allowed.example", dst.port())],
2191 };
2192
2193 assert!(!strict_hostname_allow_is_opaque(
2194 true,
2195 &policy,
2196 dst,
2197 &shared,
2198 None,
2199 b"GET / HTTP/1.1\r\n",
2200 ));
2201 }
2202
2203 #[tokio::test]
2204 async fn strict_mode_blocks_hostname_allowed_opaque_tls() {
2205 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2206 let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2207 let policy = Arc::new(NetworkPolicy {
2208 default_egress: Action::Deny,
2209 default_ingress: Action::Allow,
2210 rules: vec![allow_tcp("allowed.example", dst.port())],
2211 });
2212 let proxy_connect = Arc::new(ProxyConnectState::new());
2213 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2214 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2215
2216 from_tx
2217 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2218 .await
2219 .unwrap();
2220 drop(from_tx);
2221
2222 TcpProxy::new(
2223 dst,
2224 UpstreamTcpTarget::direct(dst),
2225 from_rx,
2226 to_tx,
2227 shared,
2228 policy,
2229 Arc::new(SecretsConfig::default()),
2230 None,
2231 true,
2232 proxy_connect.clone(),
2233 None,
2234 )
2235 .try_run()
2236 .await
2237 .unwrap();
2238
2239 assert_eq!(proxy_connect.status(), ProxyConnectStatus::PolicyDenied);
2240 }
2241
2242 #[tokio::test]
2243 async fn strict_mode_leaves_default_allowed_opaque_tls_to_policy() {
2244 let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 9);
2245 let shared = Arc::new(SharedState::new(4));
2246 let policy = Arc::new(NetworkPolicy {
2247 default_egress: Action::Allow,
2248 default_ingress: Action::Allow,
2249 rules: vec![Rule::deny_egress(Destination::Domain(
2250 "blocked.example".parse().unwrap(),
2251 ))],
2252 });
2253 let proxy_connect = Arc::new(ProxyConnectState::new());
2254 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2255 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2256
2257 from_tx
2258 .send(Bytes::from(synthetic_client_hello("allowed.example")))
2259 .await
2260 .unwrap();
2261 drop(from_tx);
2262
2263 let result = TcpProxy::new(
2264 dst,
2265 UpstreamTcpTarget::direct(dst),
2266 from_rx,
2267 to_tx,
2268 shared,
2269 policy,
2270 Arc::new(SecretsConfig::default()),
2271 None,
2272 true,
2273 proxy_connect.clone(),
2274 None,
2275 )
2276 .try_run()
2277 .await;
2278
2279 assert!(result.is_err(), "dummy upstream should refuse the dial");
2280 assert_eq!(
2281 proxy_connect.status(),
2282 ProxyConnectStatus::UpstreamConnectFailed
2283 );
2284 }
2285
2286 #[tokio::test]
2287 async fn server_first_http_like_binary_first_flight_is_forwarded() {
2288 use tokio::net::TcpListener;
2289
2290 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2291 let addr = listener.local_addr().unwrap();
2292 let server = tokio::spawn(async move {
2293 let (mut stream, _) = listener.accept().await.unwrap();
2294 stream
2295 .write_all(b"binary server-first greeting")
2296 .await
2297 .unwrap();
2298 stream.flush().await.unwrap();
2299
2300 let mut received = Vec::new();
2301 stream.read_to_end(&mut received).await.unwrap();
2302 received
2303 });
2304
2305 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2306 let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
2307 spawn_tcp_proxy(
2308 &tokio::runtime::Handle::current(),
2309 addr,
2310 addr,
2311 from_rx,
2312 to_tx,
2313 Arc::new(SharedState::new(4)),
2314 Arc::new(NetworkPolicy::default()),
2315 Arc::new(make_plain_http_secret(
2316 "$MSB_UNUSED",
2317 "unused-secret-value",
2318 false,
2319 )),
2320 None,
2321 false,
2322 Arc::new(ProxyConnectState::new()),
2323 None,
2324 );
2325
2326 let greeting = to_rx.recv().await.unwrap();
2327 assert_eq!(greeting, b"binary server-first greeting"[..]);
2328
2329 let first_flight = Bytes::from_static(b"BINARY3 v1\x00\x01opaque request");
2332 from_tx.send(first_flight.clone()).await.unwrap();
2333 drop(from_tx);
2334
2335 let wire = tokio::time::timeout(Duration::from_secs(2), server)
2336 .await
2337 .expect("proxy did not finish forwarding the client first flight")
2338 .unwrap();
2339 assert_eq!(wire, first_flight);
2340 }
2341
2342 #[tokio::test]
2343 async fn plain_http_substitutes_placeholder_when_host_arrives_in_second_segment() {
2344 let (addr, sink) = spawn_sink().await;
2347 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2348
2349 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2350 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2351 let proxy_connect = Arc::new(ProxyConnectState::new());
2352
2353 from_tx
2354 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2355 .await
2356 .unwrap();
2357 from_tx
2358 .send(Bytes::from_static(
2359 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2360 ))
2361 .await
2362 .unwrap();
2363 drop(from_tx);
2364
2365 TcpProxy::new(
2366 addr,
2367 UpstreamTcpTarget::direct(addr),
2368 from_rx,
2369 to_tx,
2370 Arc::new(SharedState::new(4)),
2371 Arc::new(NetworkPolicy::default()),
2372 Arc::new(secrets),
2373 None,
2374 false,
2375 proxy_connect,
2376 None,
2377 )
2378 .try_run()
2379 .await
2380 .unwrap();
2381
2382 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2383 assert!(wire.contains("real-secret-value"), "got: {wire:?}");
2384 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2385 }
2386
2387 #[tokio::test]
2388 async fn plain_http_passthrough_handles_a_host_in_split_headers() {
2389 let (addr, sink) = spawn_sink().await;
2393
2394 let shared = SharedState::new(4);
2395 shared.cache_resolved_hostname(
2396 "example.com",
2397 ResolvedHostnameFamily::Ipv4,
2398 ["127.0.0.1".parse::<IpAddr>().unwrap()],
2399 StdDuration::from_secs(60),
2400 );
2401
2402 let secrets = SecretsConfig {
2403 secrets: vec![SecretEntry {
2404 env_var: "API_KEY".into(),
2405 value: zeroize::Zeroizing::new("real-secret-value".into()),
2406 source: None,
2407 placeholder: "$MSB_KEY".into(),
2408 allowed_hosts: vec![HostPattern::Exact("example.com".into())],
2409 substitution: SecretSubstitution {
2410 headers: true,
2411 query: false,
2412 body: false,
2413 },
2414 passthrough_hosts: vec![HostPattern::Exact("example.com".into())],
2415 violation_action: None,
2416 require_tls_identity: true,
2417 }],
2418 ..Default::default()
2419 };
2420
2421 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2422 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2423 let proxy_connect = Arc::new(ProxyConnectState::new());
2424
2425 from_tx
2426 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2427 .await
2428 .unwrap();
2429 from_tx
2430 .send(Bytes::from_static(
2431 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2432 ))
2433 .await
2434 .unwrap();
2435 drop(from_tx);
2436
2437 TcpProxy::new(
2438 addr,
2439 UpstreamTcpTarget::direct(addr),
2440 from_rx,
2441 to_tx,
2442 Arc::new(shared),
2443 Arc::new(NetworkPolicy::default()),
2444 Arc::new(secrets),
2445 None,
2446 false,
2447 proxy_connect,
2448 None,
2449 )
2450 .try_run()
2451 .await
2452 .unwrap();
2453
2454 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2455 assert!(
2456 wire.contains("Host: example.com"),
2457 "request must reach the allowed host, got: {wire:?}"
2458 );
2459 assert!(
2460 wire.contains("$MSB_KEY"),
2461 "placeholder must be forwarded unchanged for a require_tls_identity secret, got: {wire:?}"
2462 );
2463 assert!(
2464 !wire.contains("real-secret-value"),
2465 "secret must never be substituted over plain HTTP, got: {wire:?}"
2466 );
2467 }
2468
2469 #[tokio::test]
2470 async fn plain_http_substitutes_placeholder_in_first_flight() {
2471 let (addr, sink) = spawn_sink().await;
2472
2473 let request =
2474 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2475 .to_vec();
2476 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2477
2478 let wire =
2479 String::from_utf8(relay_through_proxy(request, secrets, sink, addr).await).unwrap();
2480 assert!(
2481 wire.contains("real-secret-value"),
2482 "real value must reach server, got: {wire:?}"
2483 );
2484 assert!(
2485 !wire.contains("$MSB_KEY"),
2486 "placeholder must not reach server, got: {wire:?}"
2487 );
2488 }
2489
2490 #[tokio::test]
2491 async fn plain_http_no_substitution_when_require_tls_identity_true() {
2492 let (addr, sink) = spawn_sink().await;
2493
2494 let request =
2495 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2496 .to_vec();
2497 let mut secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", true);
2498 secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
2499
2500 let wire =
2501 String::from_utf8_lossy(&relay_through_proxy(request, secrets, sink, addr).await)
2502 .into_owned();
2503 assert!(
2504 wire.contains("$MSB_KEY"),
2505 "placeholder must be forwarded unchanged when require_tls_identity=true, got: {wire:?}"
2506 );
2507 assert!(
2508 !wire.contains("real-secret-value"),
2509 "real value must not leak when require_tls_identity=true, got: {wire:?}"
2510 );
2511 }
2512
2513 #[tokio::test]
2514 async fn plain_http_large_body_forwarded_verbatim_in_relay_loop() {
2515 let (addr, sink) = spawn_sink().await;
2519 let secrets = make_plain_http_secret("$MSB_KEY", "real-value", false);
2520
2521 let body = "x".repeat(32_000);
2522 let header = format!(
2523 "POST /upload HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\nContent-Length: {}\r\n\r\n",
2524 body.len()
2525 );
2526
2527 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2528 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2529 let proxy_connect = Arc::new(ProxyConnectState::new());
2530
2531 from_tx
2532 .send(Bytes::from(header.into_bytes()))
2533 .await
2534 .unwrap();
2535 from_tx
2536 .send(Bytes::from(body.clone().into_bytes()))
2537 .await
2538 .unwrap();
2539 drop(from_tx);
2540
2541 TcpProxy::new(
2542 addr,
2543 UpstreamTcpTarget::direct(addr),
2544 from_rx,
2545 to_tx,
2546 Arc::new(SharedState::new(4)),
2547 Arc::new(NetworkPolicy::default()),
2548 Arc::new(secrets),
2549 None,
2550 false,
2551 proxy_connect,
2552 None,
2553 )
2554 .try_run()
2555 .await
2556 .unwrap();
2557
2558 let wire = String::from_utf8_lossy(&sink.await.unwrap()).into_owned();
2559 assert!(wire.contains(&body), "got {} bytes", wire.len());
2560 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2561 }
2562}