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::netstack::shared::SharedState;
24use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
25use crate::proxy::ResolvedOutboundProxy;
26use crate::secrets::config::{SecretsConfig, SecretsConfigExt, ViolationAction};
27use crate::secrets::handler::{
28 SecretsHandler, first_line_is_not_http_request, looks_like_http_request_prefix,
29};
30use crate::tls::proxy::TlsProxy;
31use crate::tls::sni;
32use crate::tls::state::TlsState;
33
34const SERVER_READ_BUF_SIZE: usize = 16384;
40
41const CONNECT_RESP_LIMIT: usize = 8192;
43
44const PEEK_BUF_SIZE: usize = 16384;
46
47const PEEK_BUDGET: Duration = Duration::from_secs(5);
50
51#[derive(Debug)]
56struct ConnectRequest {
57 bytes: Vec<u8>,
58 header_end: usize,
59 target: ConnectTarget,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63struct ConnectTarget {
64 host: String,
65 port: u16,
66 expected_sni: Option<String>,
67}
68
69pub(crate) struct TcpProxy {
71 guest_dst: SocketAddr,
72 connect_target: UpstreamTcpTarget,
73 from_smoltcp: mpsc::Receiver<Bytes>,
74 to_smoltcp: mpsc::Sender<Bytes>,
75 shared: Arc<SharedState>,
76 network_policy: Arc<NetworkPolicy>,
77 secrets: Arc<SecretsConfig>,
78 tls_state: Option<Arc<TlsState>>,
79 proxy_connect: Arc<ProxyConnectState>,
80 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
81}
82
83impl ConnectRequest {
88 fn header_bytes(&self) -> &[u8] {
89 &self.bytes[..self.header_end]
90 }
91
92 fn post_header_bytes(&self) -> &[u8] {
93 &self.bytes[self.header_end..]
94 }
95}
96
97impl ConnectTarget {
98 fn is_intercepted(&self, tls_state: &TlsState) -> bool {
99 tls_state.config.intercepted_ports.contains(&self.port)
100 }
101
102 fn guest_dst(&self, fallback: SocketAddr, shared: &SharedState) -> SocketAddr {
103 if let Ok(ip) = self.host.parse::<IpAddr>() {
104 return SocketAddr::new(ip, self.port);
105 }
106
107 if self.host.eq_ignore_ascii_case(crate::HOST_ALIAS) {
108 match fallback.ip() {
109 IpAddr::V4(_) => {
110 if let Some(ip) = shared.gateway_ipv4() {
111 return SocketAddr::new(IpAddr::V4(ip), self.port);
112 }
113 }
114 IpAddr::V6(_) => {
115 if let Some(ip) = shared.gateway_ipv6() {
116 return SocketAddr::new(IpAddr::V6(ip), self.port);
117 }
118 }
119 }
120 if let Some(ip) = shared.gateway_ipv4() {
121 return SocketAddr::new(IpAddr::V4(ip), self.port);
122 }
123 if let Some(ip) = shared.gateway_ipv6() {
124 return SocketAddr::new(IpAddr::V6(ip), self.port);
125 }
126 }
127
128 SocketAddr::new(fallback.ip(), self.port)
129 }
130}
131
132impl TcpProxy {
133 #[allow(clippy::too_many_arguments)]
135 pub(crate) fn new(
136 guest_dst: SocketAddr,
137 connect_target: UpstreamTcpTarget,
138 from_smoltcp: mpsc::Receiver<Bytes>,
139 to_smoltcp: mpsc::Sender<Bytes>,
140 shared: Arc<SharedState>,
141 network_policy: Arc<NetworkPolicy>,
142 secrets: Arc<SecretsConfig>,
143 tls_state: Option<Arc<TlsState>>,
144 proxy_connect: Arc<ProxyConnectState>,
145 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
146 ) -> Self {
147 Self {
148 guest_dst,
149 connect_target,
150 from_smoltcp,
151 to_smoltcp,
152 shared,
153 network_policy,
154 secrets,
155 tls_state,
156 proxy_connect,
157 outbound_proxy,
158 }
159 }
160
161 pub(crate) async fn run(self) {
163 let guest_dst = self.guest_dst;
164 let connect_dst = self.connect_target.primary();
165
166 if let Err(error) = self.try_run().await {
167 tracing::debug!(
168 dst = %connect_dst,
169 %guest_dst,
170 %error,
171 "TCP proxy task ended",
172 );
173 }
174 }
175
176 async fn try_run(self) -> io::Result<()> {
178 let Self {
179 guest_dst,
180 connect_target,
181 mut from_smoltcp,
182 to_smoltcp,
183 shared,
184 network_policy,
185 secrets,
186 tls_state,
187 proxy_connect,
188 outbound_proxy,
189 } = self;
190
191 let (mut initial_buf, sni) = if network_policy.has_domain_rules() {
197 peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await
198 } else {
199 (Vec::new(), None)
200 };
201
202 if network_policy.has_domain_rules() {
208 let source = match sni.as_deref() {
209 Some(name) => HostnameSource::Sni(name),
210 None => HostnameSource::CacheOnly,
211 };
212 match network_policy.evaluate_egress_with_source(
213 guest_dst,
214 Protocol::Tcp,
215 &shared,
216 source,
217 ) {
218 EgressEvaluation::Allow => {}
219 EgressEvaluation::Deny => {
220 tracing::debug!(
221 dst = %guest_dst,
222 source = source.label(),
223 "TCP egress denied by domain policy",
224 );
225 proxy_connect.mark_policy_denied();
226 shared.proxy_wake.wake();
227 return Ok(());
228 }
229 EgressEvaluation::DeferUntilHostname => {
230 debug_assert!(false, "DeferUntilHostname leaked into TCP proxy task");
231 proxy_connect.mark_policy_denied();
232 shared.proxy_wake.wake();
233 return Ok(());
234 }
235 }
236 }
237
238 if let Some(tls_state) = tls_state.clone() {
240 if initial_buf.is_empty() {
241 let (peeked, _) = peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await;
242 initial_buf = peeked;
243 }
244 if could_be_connect_request(&initial_buf) {
245 return handle_connect_tunnel(
246 guest_dst,
247 connect_target,
248 initial_buf,
249 from_smoltcp,
250 to_smoltcp,
251 shared,
252 network_policy,
253 tls_state,
254 proxy_connect,
255 outbound_proxy,
256 None,
257 )
258 .await;
259 }
260 }
261
262 let stream = connect_target
267 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
268 .await?;
269 let connect_dst = stream.peer_addr().unwrap_or(connect_target.primary());
270 let (mut server_rx, mut server_tx) = stream.into_split();
271
272 let want_headers = secrets.has_plain_http_candidates() || secrets.has_host_scoped_secrets();
278 let (initial_buf, is_tls) = if !secrets.secrets.is_empty() {
279 classify_first_flight(
280 initial_buf,
281 &mut from_smoltcp,
282 &mut server_rx,
283 &to_smoltcp,
284 &shared,
285 want_headers,
286 PEEK_BUF_SIZE,
287 PEEK_BUDGET,
288 )
289 .await?
290 } else {
291 (initial_buf, false)
292 };
293
294 if let Some(tls_state) = tls_state.clone()
295 && could_be_connect_request(&initial_buf)
296 {
297 let proxy_stream = server_rx
302 .reunite(server_tx)
303 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
304 return handle_connect_tunnel(
305 guest_dst,
306 connect_target,
307 initial_buf,
308 from_smoltcp,
309 to_smoltcp,
310 shared,
311 network_policy,
312 tls_state,
313 proxy_connect,
314 outbound_proxy,
315 Some(proxy_stream),
316 )
317 .await;
318 }
319
320 let mut late_connect_state = tls_state;
321 let mut secrets_handler: Option<SecretsHandler> = if !secrets.secrets.is_empty() && !is_tls
322 {
323 Some(match extract_http_host(&initial_buf) {
324 Some(host) => {
325 SecretsHandler::new_plain_http(&secrets, &host, guest_dst.ip(), &shared)
326 }
327 None => SecretsHandler::new_plain_http_invalid_host(&secrets),
328 })
329 } else {
330 None
331 };
332
333 if !initial_buf.is_empty() {
335 let out: Cow<[u8]> = match secrets_handler.as_mut() {
336 Some(h) => match h.substitute(&initial_buf) {
337 Ok(cow) => cow,
340 Err(action) => {
341 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation in first flight");
342 if matches!(action, ViolationAction::BlockAndTerminate) {
343 shared.trigger_termination();
344 }
345 return Ok(());
346 }
347 },
348 None => Cow::Borrowed(&initial_buf),
349 };
350 if !out.is_empty() {
351 if let Err(e) = server_tx.write_all(&out).await {
352 tracing::debug!(dst = %connect_dst, error = %e, "replay of buffered first flight failed");
353 return Ok(());
354 }
355 if let Err(e) = server_tx.flush().await {
356 tracing::debug!(dst = %connect_dst, error = %e, "flush after first flight failed");
357 return Ok(());
358 }
359 }
360 }
361
362 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
363
364 let mut guest_eof = false;
369 loop {
370 tokio::select! {
371 data = from_smoltcp.recv(), if !guest_eof => {
373 match data {
374 Some(bytes) => {
375 if let Some(tls_state) = late_connect_state.take()
376 && could_be_connect_request(&bytes)
377 {
378 let proxy_stream = server_rx
383 .reunite(server_tx)
384 .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
385 return handle_connect_tunnel(
386 guest_dst,
387 connect_target,
388 bytes.to_vec(),
389 from_smoltcp,
390 to_smoltcp,
391 shared,
392 network_policy,
393 tls_state,
394 proxy_connect,
395 outbound_proxy,
396 Some(proxy_stream),
397 )
398 .await;
399 }
400 let out: Cow<[u8]> = match secrets_handler.as_mut() {
403 Some(h) => match h.substitute(&bytes) {
404 Ok(cow) => cow,
405 Err(action) => {
406 tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation");
407 if matches!(action, ViolationAction::BlockAndTerminate) {
408 shared.trigger_termination();
409 }
410 break;
411 }
412 },
413 None => Cow::Borrowed(&bytes),
414 };
415 if !out.is_empty() {
416 if let Err(e) = server_tx.write_all(&out).await {
417 tracing::debug!(dst = %connect_dst, error = %e, "write to server failed");
418 break;
419 }
420 if let Err(e) = server_tx.flush().await {
421 tracing::debug!(dst = %connect_dst, error = %e, "flush to server failed");
422 break;
423 }
424 }
425 }
426 None => {
431 guest_eof = true;
432 if server_tx.shutdown().await.is_err() {
433 break;
434 }
435 }
436 }
437 }
438
439 result = server_rx.read(&mut server_buf) => {
441 match result {
442 Ok(0) => break, Ok(n) => {
444 late_connect_state = None;
447 let data = Bytes::copy_from_slice(&server_buf[..n]);
448 if to_smoltcp.send(data).await.is_err() {
449 break;
451 }
452 shared.proxy_wake.wake();
455 }
456 Err(e) => {
457 tracing::debug!(dst = %connect_dst, error = %e, "read from server failed");
458 break;
459 }
460 }
461 }
462 }
463 }
464
465 Ok(())
466 }
467}
468
469#[allow(clippy::too_many_arguments)]
482pub fn spawn_tcp_proxy(
483 handle: &tokio::runtime::Handle,
484 guest_dst: SocketAddr,
485 connect_dst: SocketAddr,
486 from_smoltcp: mpsc::Receiver<Bytes>,
487 to_smoltcp: mpsc::Sender<Bytes>,
488 shared: Arc<SharedState>,
489 network_policy: Arc<NetworkPolicy>,
490 secrets: Arc<SecretsConfig>,
491 tls_state: Option<Arc<TlsState>>,
492 proxy_connect: Arc<ProxyConnectState>,
493 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
494) {
495 let proxy = TcpProxy::new(
496 guest_dst,
497 UpstreamTcpTarget::direct(connect_dst),
498 from_smoltcp,
499 to_smoltcp,
500 shared,
501 network_policy,
502 secrets,
503 tls_state,
504 proxy_connect,
505 outbound_proxy,
506 );
507
508 handle.spawn(proxy.run());
509}
510
511#[allow(clippy::too_many_arguments)]
517async fn handle_connect_tunnel(
518 guest_dst: SocketAddr,
519 proxy_target: UpstreamTcpTarget,
520 initial_buf: Vec<u8>,
521 mut from_smoltcp: mpsc::Receiver<Bytes>,
522 to_smoltcp: mpsc::Sender<Bytes>,
523 shared: Arc<SharedState>,
524 network_policy: Arc<NetworkPolicy>,
525 tls_state: Arc<TlsState>,
526 proxy_connect: Arc<ProxyConnectState>,
527 outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
528 preconnected_proxy: Option<TcpStream>,
529) -> io::Result<()> {
530 let proxy_dst = proxy_target.primary();
531 let connect_req =
532 parse_connect_request(buffer_connect_request(initial_buf, &mut from_smoltcp).await?)?;
533
534 let connect_headers = match sanitize_connect_headers(
535 connect_req.header_bytes(),
536 &tls_state.secrets.load(),
537 ) {
538 Ok(headers) => headers,
539 Err(action) => {
540 tracing::warn!(dst = %proxy_dst, violation = ?action, "secret violation in CONNECT headers");
541 if matches!(action, ViolationAction::BlockAndTerminate) {
542 shared.trigger_termination();
543 }
544 return Ok(());
545 }
546 };
547
548 let mut proxy_stream = match preconnected_proxy {
550 Some(stream) => stream,
551 None => {
552 proxy_target
553 .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
554 .await?
555 }
556 };
557
558 if !connect_req.target.is_intercepted(&tls_state) {
559 proxy_stream.write_all(&connect_headers).await?;
560 proxy_stream.flush().await?;
561 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
562 if to_smoltcp
563 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
564 .await
565 .is_err()
566 {
567 return Ok(());
568 }
569 if !proxy_resp[header_end..].is_empty()
570 && to_smoltcp
571 .send(Bytes::copy_from_slice(&proxy_resp[header_end..]))
572 .await
573 .is_err()
574 {
575 return Ok(());
576 }
577 shared.proxy_wake.wake();
578 if !connect_response_is_success(&proxy_resp[..header_end]) {
579 proxy_connect.mark_connected();
580 return Ok(());
581 }
582 if !connect_req.post_header_bytes().is_empty() {
583 proxy_stream
584 .write_all(connect_req.post_header_bytes())
585 .await?;
586 }
587 proxy_stream.flush().await?;
588 proxy_connect.mark_connected();
589 return relay_connected_stream(proxy_stream, from_smoltcp, to_smoltcp, shared).await;
590 }
591
592 proxy_stream.write_all(&connect_headers).await?;
593 proxy_stream.flush().await?;
594
595 let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
596 if !connect_response_is_success(&proxy_resp[..header_end]) {
597 return Err(io::Error::new(
598 io::ErrorKind::ConnectionRefused,
599 "proxy rejected CONNECT",
600 ));
601 }
602 if !proxy_resp[header_end..].is_empty() {
603 return Err(io::Error::new(
604 io::ErrorKind::InvalidData,
605 "proxy sent unexpected bytes after CONNECT response headers",
606 ));
607 }
608 proxy_connect.mark_connected();
609
610 if to_smoltcp
611 .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
612 .await
613 .is_err()
614 {
615 return Ok(());
616 }
617 shared.proxy_wake.wake();
618
619 let tls_seed = connect_req.post_header_bytes().to_vec();
620 let tls_guest_dst = connect_req.target.guest_dst(guest_dst, &shared);
621 let expected_sni = connect_req.target.expected_sni.clone();
622
623 TlsProxy::new(
624 tls_guest_dst,
625 proxy_target,
626 from_smoltcp,
627 to_smoltcp,
628 shared,
629 tls_state,
630 network_policy,
631 proxy_connect,
632 None,
636 )
637 .with_upstream(proxy_stream)
638 .with_expected_sni(expected_sni)
639 .with_initial_buf(tls_seed)
640 .try_run()
641 .await
642}
643
644async fn relay_connected_stream(
646 stream: TcpStream,
647 mut from_smoltcp: mpsc::Receiver<Bytes>,
648 to_smoltcp: mpsc::Sender<Bytes>,
649 shared: Arc<SharedState>,
650) -> io::Result<()> {
651 let (mut server_rx, mut server_tx) = stream.into_split();
652 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
653
654 let mut guest_eof = false;
655 loop {
656 tokio::select! {
657 data = from_smoltcp.recv(), if !guest_eof => {
658 match data {
659 Some(bytes) => {
660 server_tx.write_all(&bytes).await?;
661 server_tx.flush().await?;
662 }
663 None => {
666 guest_eof = true;
667 if server_tx.shutdown().await.is_err() {
668 break;
669 }
670 }
671 }
672 }
673 result = server_rx.read(&mut server_buf) => {
674 match result {
675 Ok(0) => break,
676 Ok(n) => {
677 if to_smoltcp
678 .send(Bytes::copy_from_slice(&server_buf[..n]))
679 .await
680 .is_err()
681 {
682 break;
683 }
684 shared.proxy_wake.wake();
685 }
686 Err(e) => return Err(e),
687 }
688 }
689 }
690 }
691
692 Ok(())
693}
694
695async fn buffer_connect_request(
696 mut buf: Vec<u8>,
697 from_smoltcp: &mut mpsc::Receiver<Bytes>,
698) -> io::Result<Vec<u8>> {
699 let timeout_fut = tokio::time::sleep(PEEK_BUDGET);
700 tokio::pin!(timeout_fut);
701
702 loop {
703 if !could_be_connect_request(&buf) {
704 return Err(io::Error::new(
705 io::ErrorKind::InvalidData,
706 "malformed CONNECT request prefix",
707 ));
708 }
709 if headers_end(&buf).is_some() {
710 return Ok(buf);
711 }
712 if buf.len() >= PEEK_BUF_SIZE {
713 return Err(io::Error::new(
714 io::ErrorKind::InvalidData,
715 "CONNECT request headers too large",
716 ));
717 }
718
719 tokio::select! {
720 biased;
721 _ = &mut timeout_fut => {
722 return Err(io::Error::new(
723 io::ErrorKind::TimedOut,
724 "timed out waiting for complete CONNECT request headers",
725 ));
726 }
727 data = from_smoltcp.recv() => match data {
728 Some(bytes) => {
729 buf.extend_from_slice(&bytes);
730 }
731 None => {
732 return Err(io::Error::new(
733 io::ErrorKind::UnexpectedEof,
734 "channel closed before complete CONNECT request headers",
735 ));
736 }
737 }
738 }
739 }
740}
741
742async fn read_connect_response_headers(stream: &mut TcpStream) -> io::Result<(Vec<u8>, usize)> {
743 tokio::time::timeout(PEEK_BUDGET, async {
744 let mut proxy_resp = Vec::with_capacity(256);
745 let mut buf = [0u8; 4096];
746 loop {
747 let n = stream.read(&mut buf).await?;
748 if n == 0 {
749 return Err(io::Error::new(
750 io::ErrorKind::UnexpectedEof,
751 "proxy closed before sending CONNECT response",
752 ));
753 }
754 proxy_resp.extend_from_slice(&buf[..n]);
755 if let Some(end) = headers_end(&proxy_resp) {
756 return Ok((proxy_resp, end));
757 }
758 if proxy_resp.len() > CONNECT_RESP_LIMIT {
759 return Err(io::Error::new(
760 io::ErrorKind::InvalidData,
761 "proxy CONNECT response too large",
762 ));
763 }
764 }
765 })
766 .await
767 .map_err(|_| {
768 io::Error::new(
769 io::ErrorKind::TimedOut,
770 "timed out waiting for proxy CONNECT response",
771 )
772 })?
773}
774
775fn sanitize_connect_headers<'a>(
776 header_bytes: &'a [u8],
777 secrets: &SecretsConfig,
778) -> Result<Cow<'a, [u8]>, ViolationAction> {
779 if secrets.secrets.is_empty() {
780 return Ok(Cow::Borrowed(header_bytes));
781 }
782
783 let mut handler = SecretsHandler::new_plain_http_untrusted_metadata(secrets);
784 handler.substitute(header_bytes)
785}
786
787fn headers_end(buf: &[u8]) -> Option<usize> {
789 buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
790}
791
792fn could_be_connect_request(buf: &[u8]) -> bool {
793 const PREFIX: &[u8] = b"CONNECT ";
794 if buf.is_empty() {
795 return false;
796 }
797 let n = buf.len().min(PREFIX.len());
798 buf[..n].eq_ignore_ascii_case(&PREFIX[..n])
799}
800
801fn parse_connect_request(bytes: Vec<u8>) -> io::Result<ConnectRequest> {
802 let header_end = headers_end(&bytes).ok_or_else(|| {
803 io::Error::new(
804 io::ErrorKind::InvalidData,
805 "incomplete CONNECT request headers",
806 )
807 })?;
808 let target = {
809 let request_line = bytes[..header_end]
810 .split(|&b| b == b'\n')
811 .next()
812 .unwrap_or(&[]);
813 let request_line = std::str::from_utf8(request_line)
814 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "CONNECT line is not UTF-8"))?
815 .trim_end_matches('\r');
816 let mut parts = request_line.split_ascii_whitespace();
817 let method = parts.next().unwrap_or_default();
818 let authority = parts.next().unwrap_or_default();
819 let version = parts.next().unwrap_or_default();
820 if !method.eq_ignore_ascii_case("CONNECT")
821 || authority.is_empty()
822 || !is_http_version(version)
823 || parts.next().is_some()
824 {
825 return Err(io::Error::new(
826 io::ErrorKind::InvalidData,
827 "malformed CONNECT request line",
828 ));
829 }
830 parse_connect_target(authority)?
831 };
832
833 Ok(ConnectRequest {
834 bytes,
835 header_end,
836 target,
837 })
838}
839
840fn parse_connect_target(authority: &str) -> io::Result<ConnectTarget> {
841 let authority = authority.trim();
842 let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
843 let (host, rest) = rest.split_once(']').ok_or_else(|| {
844 io::Error::new(
845 io::ErrorKind::InvalidData,
846 "malformed CONNECT IPv6 authority",
847 )
848 })?;
849 let port = rest.strip_prefix(':').ok_or_else(|| {
850 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
851 })?;
852 (host, port)
853 } else {
854 let (host, port) = authority.rsplit_once(':').ok_or_else(|| {
855 io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
856 })?;
857 if host.contains(':') {
858 return Err(io::Error::new(
859 io::ErrorKind::InvalidData,
860 "CONNECT IPv6 authority must be bracketed",
861 ));
862 }
863 (host, port)
864 };
865 let host = host.trim().trim_end_matches('.');
866 if host.is_empty() {
867 return Err(io::Error::new(
868 io::ErrorKind::InvalidData,
869 "CONNECT authority missing host",
870 ));
871 }
872 let port = port
873 .parse::<u16>()
874 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid CONNECT port"))?;
875 let expected_sni = host
876 .parse::<IpAddr>()
877 .is_err()
878 .then(|| host.to_ascii_lowercase());
879
880 Ok(ConnectTarget {
881 host: host.to_ascii_lowercase(),
882 port,
883 expected_sni,
884 })
885}
886
887fn is_http_version(version: &str) -> bool {
888 let Some(version) = version.strip_prefix("HTTP/") else {
889 return false;
890 };
891 let Some((major, minor)) = version.split_once('.') else {
892 return false;
893 };
894 !major.is_empty()
895 && !minor.is_empty()
896 && major.bytes().all(|b| b.is_ascii_digit())
897 && minor.bytes().all(|b| b.is_ascii_digit())
898}
899
900fn connect_response_is_success(headers: &[u8]) -> bool {
901 let Some(status_line) = headers.split(|&b| b == b'\n').next() else {
902 return false;
903 };
904 let Ok(status_line) = std::str::from_utf8(status_line) else {
905 return false;
906 };
907 let mut parts = status_line.trim_end_matches('\r').split_ascii_whitespace();
908 let version = parts.next().unwrap_or_default();
909 let status = parts.next().unwrap_or_default();
910 is_http_version(version)
911 && status.len() == 3
912 && status
913 .parse::<u16>()
914 .is_ok_and(|code| (200..300).contains(&code))
915}
916
917fn extract_http_host(buf: &[u8]) -> Option<String> {
927 if buf.first() == Some(&0x16) {
928 return None;
929 }
930 let mut headers = vec![httparse::EMPTY_HEADER; (buf.len() / 4).max(16)];
936 let mut req = httparse::Request::new(&mut headers);
937 req.parse(buf).ok()?;
938 req.headers
939 .iter()
940 .find(|h| h.name.eq_ignore_ascii_case("host"))
941 .and_then(|h| std::str::from_utf8(h.value).ok())
942 .map(|v| {
943 let host = v.trim();
944 host.rsplit_once(':')
946 .map(|(h, _)| h)
947 .unwrap_or(host)
948 .to_ascii_lowercase()
949 })
950 .filter(|h| !h.is_empty())
951}
952
953#[allow(clippy::too_many_arguments)]
970async fn classify_first_flight(
971 mut buf: Vec<u8>,
972 from_smoltcp: &mut mpsc::Receiver<Bytes>,
973 server_rx: &mut tokio::net::tcp::OwnedReadHalf,
974 to_smoltcp: &mpsc::Sender<Bytes>,
975 shared: &SharedState,
976 want_headers: bool,
977 max: usize,
978 budget: Duration,
979) -> io::Result<(Vec<u8>, bool)> {
980 let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
981 let timeout_fut = tokio::time::sleep(budget);
982 tokio::pin!(timeout_fut);
983
984 loop {
985 if !buf.is_empty() {
991 let is_tls = buf.first() == Some(&0x16);
992 let not_http = !is_tls
993 && (!looks_like_http_request_prefix(&buf) || first_line_is_not_http_request(&buf));
994 let done = !want_headers
995 || is_tls
996 || not_http
997 || buf.len() >= max
998 || buf.windows(4).any(|w| w == b"\r\n\r\n");
999 if done {
1000 return Ok((buf, is_tls));
1001 }
1002 }
1003
1004 tokio::select! {
1005 biased;
1006 _ = &mut timeout_fut => {
1007 let is_tls = buf.first() == Some(&0x16);
1008 return Ok((buf, is_tls));
1009 }
1010 guest = from_smoltcp.recv() => match guest {
1013 Some(bytes) => buf.extend_from_slice(&bytes),
1014 None => {
1015 let is_tls = buf.first() == Some(&0x16);
1016 return Ok((buf, is_tls));
1017 }
1018 },
1019 server = server_rx.read(&mut server_buf) => match server {
1022 Ok(0) => {
1023 let is_tls = buf.first() == Some(&0x16);
1024 return Ok((buf, is_tls));
1025 }
1026 Ok(n) => {
1027 let data = Bytes::copy_from_slice(&server_buf[..n]);
1028 if to_smoltcp.send(data).await.is_err() {
1029 let is_tls = buf.first() == Some(&0x16);
1030 return Ok((buf, is_tls));
1031 }
1032 shared.proxy_wake.wake();
1033 }
1034 Err(e) => return Err(e),
1035 },
1036 }
1037 }
1038}
1039
1040async fn peek_for_sni(
1050 rx: &mut mpsc::Receiver<Bytes>,
1051 max: usize,
1052 budget: Duration,
1053) -> (Vec<u8>, Option<String>) {
1054 let mut buf = Vec::with_capacity(PEEK_BUF_SIZE.min(8192));
1055 let timeout_fut = tokio::time::sleep(budget);
1056 tokio::pin!(timeout_fut);
1057
1058 let raw_sni = loop {
1059 tokio::select! {
1060 biased;
1061 _ = &mut timeout_fut => break None,
1062 data = rx.recv() => {
1063 match data {
1064 Some(bytes) => {
1065 buf.extend_from_slice(&bytes);
1066 if buf.first() != Some(&0x16) {
1071 break None;
1072 }
1073 if let Some(name) = sni::extract_sni(&buf) {
1074 break Some(name);
1075 }
1076 if buf.len() >= max {
1077 break None;
1078 }
1079 }
1080 None => break None,
1081 }
1082 }
1083 }
1084 };
1085
1086 let canonical = raw_sni.map(|s| s.trim_end_matches('.').to_ascii_lowercase());
1087 (buf, canonical)
1088}
1089
1090#[cfg(test)]
1095mod tests {
1096 use super::*;
1097
1098 fn synthetic_client_hello(sni: &str) -> Vec<u8> {
1102 let host_bytes = sni.as_bytes();
1105 let host_len = host_bytes.len() as u16;
1106 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();
1111 body.extend_from_slice(&[0x03, 0x03]);
1113 body.extend_from_slice(&[0u8; 32]);
1115 body.push(0);
1117 body.extend_from_slice(&[0x00, 0x02, 0x00, 0x2f]);
1119 body.extend_from_slice(&[0x01, 0x00]);
1121 body.extend_from_slice(&extensions_total.to_be_bytes());
1123 body.extend_from_slice(&[0x00, 0x00]);
1125 body.extend_from_slice(&extension_data_len.to_be_bytes());
1126 body.extend_from_slice(&server_name_list_len.to_be_bytes());
1127 body.push(0x00); body.extend_from_slice(&host_len.to_be_bytes());
1129 body.extend_from_slice(host_bytes);
1130
1131 let handshake_len = body.len() as u32;
1132 let mut hs = Vec::new();
1133 hs.push(0x01); hs.extend_from_slice(&handshake_len.to_be_bytes()[1..]); hs.extend_from_slice(&body);
1136
1137 let record_len = hs.len() as u16;
1138 let mut record = Vec::new();
1139 record.extend_from_slice(&[0x16, 0x03, 0x01]); record.extend_from_slice(&record_len.to_be_bytes());
1141 record.extend_from_slice(&hs);
1142
1143 record
1144 }
1145
1146 #[tokio::test]
1147 async fn connect_upstream_dials_target_directly_without_outbound_proxy() {
1148 use tokio::net::TcpListener;
1149
1150 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1151 let addr = listener.local_addr().unwrap();
1152 let accept = tokio::spawn(async move {
1153 let (mut sock, _) = listener.accept().await.unwrap();
1154 let mut buf = [0u8; 5];
1155 sock.read_exact(&mut buf).await.unwrap();
1156 assert_eq!(&buf, b"hello");
1157 });
1158
1159 let shared = SharedState::new(4);
1160 let proxy_connect = ProxyConnectState::new();
1161 let mut stream = UpstreamTcpTarget::direct(addr)
1162 .connect(&proxy_connect, &shared, None)
1163 .await
1164 .unwrap();
1165 stream.write_all(b"hello").await.unwrap();
1166
1167 accept.await.unwrap();
1168 assert!(matches!(
1169 proxy_connect.status(),
1170 ProxyConnectStatus::Connected
1171 ));
1172 }
1173
1174 #[tokio::test]
1175 async fn early_http_connect_dials_proxy_through_configured_socks5_proxy() {
1176 let _ = rustls::crypto::ring::default_provider().install_default();
1177
1178 let http_proxy_addr: SocketAddr = "93.184.216.34:3128".parse().unwrap();
1181 let socks_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1182 let outbound_proxy = ResolvedOutboundProxy::Socks5 {
1183 address: socks_listener.local_addr().unwrap(),
1184 credentials: None,
1185 };
1186 let socks_task = tokio::spawn(async move {
1187 let (mut client, _) = socks_listener.accept().await.unwrap();
1188
1189 let mut greeting = [0u8; 3];
1190 client.read_exact(&mut greeting).await.unwrap();
1191 assert_eq!(greeting, [0x05, 0x01, 0x00]);
1192 client.write_all(&[0x05, 0x00]).await.unwrap();
1193
1194 let mut socks_request = [0u8; 10];
1195 client.read_exact(&mut socks_request).await.unwrap();
1196 assert_eq!(socks_request[0..4], [0x05, 0x01, 0x00, 0x01]);
1197 assert_eq!(&socks_request[4..8], &[93, 184, 216, 34]);
1198 assert_eq!(
1199 u16::from_be_bytes([socks_request[8], socks_request[9]]),
1200 3128
1201 );
1202 client
1203 .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1204 .await
1205 .unwrap();
1206
1207 let expected_connect =
1208 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
1209 let mut connect_request = vec![0u8; expected_connect.len()];
1210 client.read_exact(&mut connect_request).await.unwrap();
1211 assert_eq!(&connect_request, expected_connect);
1212 client
1213 .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
1214 .await
1215 .unwrap();
1216 });
1217
1218 let connect_request =
1219 b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n".to_vec();
1220 let (from_tx, from_rx) = mpsc::channel(1);
1221 let (to_tx, mut to_rx) = mpsc::channel(1);
1222 drop(from_tx);
1223
1224 let tls_state = Arc::new(
1225 TlsState::new(
1226 microsandbox_types::TlsConfig::default(),
1227 crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1228 )
1229 .unwrap(),
1230 );
1231 let proxy_connect = Arc::new(ProxyConnectState::new());
1232
1233 handle_connect_tunnel(
1234 http_proxy_addr,
1235 UpstreamTcpTarget::direct(http_proxy_addr),
1236 connect_request,
1237 from_rx,
1238 to_tx,
1239 Arc::new(SharedState::new(4)),
1240 Arc::new(NetworkPolicy::default()),
1241 tls_state,
1242 proxy_connect.clone(),
1243 Some(Arc::new(outbound_proxy)),
1244 None,
1245 )
1246 .await
1247 .unwrap();
1248
1249 let response = to_rx.recv().await.unwrap();
1250 assert_eq!(
1251 &response[..],
1252 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1253 );
1254 socks_task.await.unwrap();
1255 assert!(matches!(
1256 proxy_connect.status(),
1257 ProxyConnectStatus::Connected
1258 ));
1259 }
1260
1261 #[test]
1262 fn could_be_connect_request_matches_split_prefixes_only() {
1263 assert!(could_be_connect_request(b"C"));
1264 assert!(could_be_connect_request(b"connect "));
1265 assert!(could_be_connect_request(b"CONNECT example.com:443"));
1266 assert!(!could_be_connect_request(b"CLIENT"));
1267 assert!(!could_be_connect_request(b"GET / HTTP/1.1\r\n"));
1268 }
1269
1270 #[tokio::test]
1271 async fn buffer_connect_request_reads_split_headers() {
1272 let (tx, mut rx) = mpsc::channel(4);
1273 tx.send(Bytes::from_static(b"NECT example.com:443 HTTP/1.1\r\n"))
1274 .await
1275 .unwrap();
1276 tx.send(Bytes::from_static(b"Host: example.com\r\n\r\n"))
1277 .await
1278 .unwrap();
1279 drop(tx);
1280
1281 let buffered = buffer_connect_request(b"CON".to_vec(), &mut rx)
1282 .await
1283 .unwrap();
1284 let parsed = parse_connect_request(buffered).unwrap();
1285
1286 assert_eq!(parsed.target.host, "example.com");
1287 assert_eq!(parsed.target.port, 443);
1288 assert_eq!(parsed.target.expected_sni.as_deref(), Some("example.com"));
1289 assert!(parsed.post_header_bytes().is_empty());
1290 }
1291
1292 #[test]
1293 fn parse_connect_request_preserves_post_header_tls_seed() {
1294 let mut request = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
1295 request.extend_from_slice(b"\x16\x03\x01client-hello");
1296
1297 let parsed = parse_connect_request(request).unwrap();
1298
1299 assert_eq!(
1300 parsed.header_bytes(),
1301 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n"
1302 );
1303 assert_eq!(parsed.post_header_bytes(), b"\x16\x03\x01client-hello");
1304 }
1305
1306 #[test]
1307 fn parse_connect_target_requires_authority_port() {
1308 assert!(parse_connect_target("example.com").is_err());
1309 assert!(parse_connect_target("2001:db8::1:443").is_err());
1310
1311 let target = parse_connect_target("[2001:db8::1]:8443").unwrap();
1312 assert_eq!(target.host, "2001:db8::1");
1313 assert_eq!(target.port, 8443);
1314 assert_eq!(target.expected_sni, None);
1315 }
1316
1317 #[test]
1318 fn connect_response_success_requires_exact_2xx_status_code() {
1319 assert!(connect_response_is_success(
1320 b"HTTP/1.1 200 Connection Established\r\n\r\n"
1321 ));
1322 assert!(connect_response_is_success(
1323 b"HTTP/1.1 204 Connection Established\r\n\r\n"
1324 ));
1325 assert!(!connect_response_is_success(b"HTTP/1.1 2000 Weird\r\n\r\n"));
1326 assert!(!connect_response_is_success(b"HTTP/1.1 199 Nope\r\n\r\n"));
1327 assert!(!connect_response_is_success(b"NOTHTTP 200 OK\r\n\r\n"));
1328 }
1329
1330 #[tokio::test]
1331 async fn peek_for_sni_extracts_and_canonicalizes() {
1332 let (tx, mut rx) = mpsc::channel(4);
1333 let hello = synthetic_client_hello("Example.COM");
1334 tx.send(Bytes::from(hello.clone())).await.unwrap();
1335 drop(tx); let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1338 assert_eq!(sni.as_deref(), Some("example.com"));
1339 assert_eq!(buf, hello);
1340 }
1341
1342 #[tokio::test]
1343 async fn peek_for_sni_returns_none_on_channel_close_without_data() {
1344 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1345 drop(tx);
1346 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1347 assert!(buf.is_empty());
1348 assert_eq!(sni, None);
1349 }
1350
1351 #[tokio::test]
1352 async fn peek_for_sni_returns_none_on_non_tls_data() {
1353 let (tx, mut rx) = mpsc::channel(4);
1354 tx.send(Bytes::from_static(
1356 b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
1357 ))
1358 .await
1359 .unwrap();
1360 drop(tx);
1361 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1362 assert!(
1363 !buf.is_empty(),
1364 "buffered bytes must be returned for replay"
1365 );
1366 assert_eq!(sni, None);
1367 }
1368
1369 #[tokio::test]
1370 async fn peek_for_sni_falls_back_on_timeout() {
1371 let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1372 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, Duration::from_millis(50)).await;
1374 drop(tx);
1375 assert!(buf.is_empty());
1376 assert_eq!(sni, None);
1377 }
1378
1379 #[tokio::test]
1380 async fn peek_for_sni_caps_at_max_bytes() {
1381 let (tx, mut rx) = mpsc::channel(4);
1382 let mut first = vec![0u8; 8192];
1386 first[0] = 0x16;
1387 tx.send(Bytes::from(first)).await.unwrap();
1388 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1389 tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1390 drop(tx);
1391
1392 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1393 assert_eq!(sni, None, "no SNI in non-TLS data");
1394 assert!(
1395 buf.len() >= PEEK_BUF_SIZE,
1396 "buffer must hit the cap before bail-out: got {}",
1397 buf.len()
1398 );
1399 }
1400
1401 #[tokio::test]
1402 async fn peek_for_sni_bails_immediately_on_non_tls_first_byte() {
1403 let (tx, mut rx) = mpsc::channel(4);
1404 tx.send(Bytes::from_static(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
1406 .await
1407 .unwrap();
1408 drop(tx);
1409
1410 let started = std::time::Instant::now();
1413 let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1414 let elapsed = started.elapsed();
1415 assert_eq!(sni, None);
1416 assert!(buf.starts_with(b"GET"));
1417 assert!(
1418 elapsed < Duration::from_millis(500),
1419 "non-TLS bail must be fast: took {elapsed:?}"
1420 );
1421 }
1422
1423 use std::net::IpAddr;
1428 use std::time::Duration as StdDuration;
1429
1430 use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
1431 use crate::policy::{Action, Destination, NetworkPolicy, PortRange, Rule};
1432
1433 const SHARED_FASTLY_IP: &str = "151.101.0.223";
1434
1435 fn shared_with(host: &str, ip: &str) -> SharedState {
1436 let shared = SharedState::new(4);
1437 shared.cache_resolved_hostname(
1438 host,
1439 ResolvedHostnameFamily::Ipv4,
1440 [ip.parse::<IpAddr>().unwrap()],
1441 StdDuration::from_secs(60),
1442 );
1443 shared
1444 }
1445
1446 fn allow_https(domain: &str) -> Rule {
1447 Rule {
1448 direction: crate::policy::Direction::Egress,
1449 destination: Destination::Domain(domain.parse().unwrap()),
1450 protocols: vec![Protocol::Tcp],
1451 ports: vec![PortRange::single(443)],
1452 action: Action::Allow,
1453 }
1454 }
1455
1456 #[tokio::test]
1459 async fn integration_sni_overrides_cache_for_over_allow() {
1460 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1461 let policy = NetworkPolicy {
1462 default_egress: Action::Deny,
1463 default_ingress: Action::Allow,
1464 rules: vec![allow_https("pypi.org")],
1465 };
1466 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1467
1468 let (tx, mut rx) = mpsc::channel(4);
1469 tx.send(Bytes::from(synthetic_client_hello("evil.com")))
1470 .await
1471 .unwrap();
1472 drop(tx);
1473
1474 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1475 assert_eq!(sni.as_deref(), Some("evil.com"));
1476 assert!(!initial_buf.is_empty());
1477
1478 let source = sni
1479 .as_deref()
1480 .map(HostnameSource::Sni)
1481 .unwrap_or(HostnameSource::CacheOnly);
1482 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1483 assert_eq!(
1484 eval,
1485 EgressEvaluation::Deny,
1486 "SNI=evil.com must not piggy-back on the cached pypi.org match",
1487 );
1488 }
1489
1490 #[tokio::test]
1493 async fn integration_sni_overrides_cache_for_over_block() {
1494 let shared = shared_with("ads.example.com", SHARED_FASTLY_IP);
1495 let policy = NetworkPolicy {
1496 default_egress: Action::Allow,
1497 default_ingress: Action::Allow,
1498 rules: vec![Rule::deny_egress(Destination::Domain(
1499 "ads.example.com".parse().unwrap(),
1500 ))],
1501 };
1502 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1503
1504 let (tx, mut rx) = mpsc::channel(4);
1505 tx.send(Bytes::from(synthetic_client_hello("api.example.com")))
1506 .await
1507 .unwrap();
1508 drop(tx);
1509
1510 let (_initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1511 assert_eq!(sni.as_deref(), Some("api.example.com"));
1512
1513 let source = sni
1514 .as_deref()
1515 .map(HostnameSource::Sni)
1516 .unwrap_or(HostnameSource::CacheOnly);
1517 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1518 assert_eq!(
1519 eval,
1520 EgressEvaluation::Allow,
1521 "SNI=api.example.com must not be caught by the deny on ads.example.com",
1522 );
1523 }
1524
1525 #[tokio::test]
1528 async fn integration_non_tls_falls_back_to_cache() {
1529 let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1530 let policy = NetworkPolicy {
1531 default_egress: Action::Deny,
1532 default_ingress: Action::Allow,
1533 rules: vec![allow_https("pypi.org")],
1534 };
1535 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1536
1537 let (tx, mut rx) = mpsc::channel(4);
1538 tx.send(Bytes::from_static(
1540 b"GET / HTTP/1.1\r\nHost: pypi.org\r\n\r\n",
1541 ))
1542 .await
1543 .unwrap();
1544 drop(tx);
1545
1546 let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1547 assert_eq!(sni, None, "non-TLS data → no SNI");
1548 assert!(
1549 !initial_buf.is_empty(),
1550 "buffered bytes must survive for replay"
1551 );
1552
1553 let source = sni
1554 .as_deref()
1555 .map(HostnameSource::Sni)
1556 .unwrap_or(HostnameSource::CacheOnly);
1557 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1558 assert_eq!(
1559 eval,
1560 EgressEvaluation::Allow,
1561 "cache-only fallback must still allow the cached hostname's IP",
1562 );
1563 }
1564
1565 #[tokio::test]
1568 async fn integration_sni_matches_domain_suffix_with_cache_binding() {
1569 let shared = shared_with("files.pythonhosted.org", SHARED_FASTLY_IP);
1570 let policy = NetworkPolicy {
1571 default_egress: Action::Deny,
1572 default_ingress: Action::Allow,
1573 rules: vec![Rule {
1574 direction: crate::policy::Direction::Egress,
1575 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1576 protocols: vec![Protocol::Tcp],
1577 ports: vec![PortRange::single(443)],
1578 action: Action::Allow,
1579 }],
1580 };
1581 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1582
1583 let (tx, mut rx) = mpsc::channel(4);
1584 tx.send(Bytes::from(synthetic_client_hello(
1585 "files.pythonhosted.org",
1586 )))
1587 .await
1588 .unwrap();
1589 drop(tx);
1590
1591 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1592 let source = sni
1593 .as_deref()
1594 .map(HostnameSource::Sni)
1595 .unwrap_or(HostnameSource::CacheOnly);
1596 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1597 assert_eq!(eval, EgressEvaluation::Allow);
1598 }
1599
1600 #[tokio::test]
1605 async fn integration_sni_denies_domain_suffix_without_cache_binding() {
1606 let shared = SharedState::new(4); let policy = NetworkPolicy {
1608 default_egress: Action::Deny,
1609 default_ingress: Action::Allow,
1610 rules: vec![Rule {
1611 direction: crate::policy::Direction::Egress,
1612 destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1613 protocols: vec![Protocol::Tcp],
1614 ports: vec![PortRange::single(443)],
1615 action: Action::Allow,
1616 }],
1617 };
1618 let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1619
1620 let (tx, mut rx) = mpsc::channel(4);
1621 tx.send(Bytes::from(synthetic_client_hello(
1622 "files.pythonhosted.org",
1623 )))
1624 .await
1625 .unwrap();
1626 drop(tx);
1627
1628 let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1629 let source = sni
1630 .as_deref()
1631 .map(HostnameSource::Sni)
1632 .unwrap_or(HostnameSource::CacheOnly);
1633 let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1634 assert_eq!(eval, EgressEvaluation::Deny);
1635 }
1636
1637 #[test]
1640 fn extract_http_host_basic() {
1641 let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1642 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1643 }
1644
1645 #[test]
1646 fn extract_http_host_strips_port() {
1647 let buf = b"POST /api HTTP/1.1\r\nHost: api.company.com:8080\r\n\r\n";
1648 assert_eq!(extract_http_host(buf), Some("api.company.com".into()));
1649 }
1650
1651 #[test]
1652 fn extract_http_host_case_insensitive_lowercased() {
1653 let buf = b"GET / HTTP/1.1\r\nhost: Example.COM\r\n\r\n";
1654 assert_eq!(extract_http_host(buf), Some("example.com".into()));
1655 }
1656
1657 #[test]
1658 fn extract_http_host_no_host_header() {
1659 let buf = b"GET / HTTP/1.1\r\nX-Other: foo\r\n\r\n";
1660 assert_eq!(extract_http_host(buf), None);
1661 }
1662
1663 #[test]
1664 fn extract_http_host_incomplete_headers() {
1665 let buf = b"GET / HTTP/1.1\r\nHost: x";
1666 assert_eq!(extract_http_host(buf), None);
1667 }
1668
1669 #[test]
1670 fn extract_http_host_tls_first_byte() {
1671 let buf = [0x16u8, 0x03, 0x01, 0x00, 0x01];
1672 assert_eq!(extract_http_host(&buf), None);
1673 }
1674
1675 #[test]
1676 fn extract_http_host_with_many_headers() {
1677 let mut req = Vec::from(&b"GET / HTTP/1.1\r\n"[..]);
1680 for i in 0..100 {
1681 req.extend_from_slice(format!("X-Pad-{i}: v\r\n").as_bytes());
1682 }
1683 req.extend_from_slice(b"Host: example.com\r\n\r\n");
1684 assert_eq!(extract_http_host(&req), Some("example.com".into()));
1685 }
1686
1687 use std::sync::Arc;
1690 use tokio::io::AsyncReadExt;
1691 use tokio::net::TcpListener;
1692 use tokio::task::JoinHandle;
1693
1694 use crate::secrets::config::{HostPattern, SecretEntry, SecretInjection, SecretsConfig};
1695
1696 fn make_plain_http_secret(placeholder: &str, value: &str, require_tls: bool) -> SecretsConfig {
1697 SecretsConfig {
1698 secrets: vec![SecretEntry {
1699 env_var: "API_KEY".into(),
1700 value: zeroize::Zeroizing::new(value.into()),
1701 source: None,
1702 placeholder: placeholder.into(),
1703 allowed_hosts: vec![HostPattern::Any],
1704 injection: SecretInjection {
1705 headers: true,
1706 basic_auth: false,
1707 query_params: false,
1708 body: false,
1709 },
1710 on_violation: None,
1711 require_tls_identity: require_tls,
1712 }],
1713 ..Default::default()
1714 }
1715 }
1716
1717 fn make_host_bound_secret(placeholder: &str, value: &str, host: &str) -> SecretsConfig {
1718 SecretsConfig {
1719 secrets: vec![SecretEntry {
1720 env_var: "API_KEY".into(),
1721 value: zeroize::Zeroizing::new(value.into()),
1722 source: None,
1723 placeholder: placeholder.into(),
1724 allowed_hosts: vec![HostPattern::Exact(host.into())],
1725 injection: SecretInjection::default(),
1726 on_violation: None,
1727 require_tls_identity: true,
1728 }],
1729 ..Default::default()
1730 }
1731 }
1732
1733 #[test]
1734 fn sanitize_connect_headers_blocks_placeholder_metadata_header_by_default() {
1735 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1736 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";
1737
1738 assert_eq!(
1739 sanitize_connect_headers(headers, &secrets),
1740 Err(ViolationAction::BlockAndLog)
1741 );
1742 }
1743
1744 #[test]
1745 fn sanitize_connect_headers_respects_block_and_terminate() {
1746 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1747 secrets.on_violation = ViolationAction::BlockAndTerminate;
1748 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";
1749
1750 assert_eq!(
1751 sanitize_connect_headers(headers, &secrets),
1752 Err(ViolationAction::BlockAndTerminate)
1753 );
1754 }
1755
1756 #[test]
1757 fn sanitize_connect_headers_respects_explicit_passthrough() {
1758 let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1759 secrets.on_violation = ViolationAction::Passthrough(vec![HostPattern::Any]);
1760 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";
1761
1762 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1763
1764 assert_eq!(sanitized.as_ref(), headers);
1765 assert!(
1766 !String::from_utf8_lossy(sanitized.as_ref()).contains("real-secret-value"),
1767 "passthrough must never substitute real secrets into CONNECT metadata"
1768 );
1769 }
1770
1771 #[test]
1772 fn sanitize_connect_headers_keeps_safe_metadata_headers() {
1773 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1774 let headers =
1775 b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nUser-Agent: curl\r\n\r\n";
1776
1777 let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1778
1779 assert_eq!(sanitized.as_ref(), headers);
1780 }
1781
1782 #[test]
1783 fn sanitize_connect_headers_blocks_placeholder_in_request_line() {
1784 let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1785 let headers = b"CONNECT $MSB_KEY:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
1786
1787 assert_eq!(
1788 sanitize_connect_headers(headers, &secrets),
1789 Err(ViolationAction::BlockAndLog)
1790 );
1791 }
1792
1793 async fn spawn_sink() -> (SocketAddr, JoinHandle<Vec<u8>>) {
1794 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1795 let addr = listener.local_addr().unwrap();
1796 let handle = tokio::spawn(async move {
1797 let (mut stream, _) = listener.accept().await.unwrap();
1798 let mut received = Vec::new();
1799 let mut buf = vec![0u8; 4096];
1800 loop {
1801 match stream.read(&mut buf).await {
1802 Ok(0) | Err(_) => break,
1803 Ok(n) => received.extend_from_slice(&buf[..n]),
1804 }
1805 }
1806 received
1807 });
1808 (addr, handle)
1809 }
1810
1811 async fn relay_through_proxy(
1812 request: Vec<u8>,
1813 secrets: SecretsConfig,
1814 handle: JoinHandle<Vec<u8>>,
1815 server_addr: SocketAddr,
1816 ) -> Vec<u8> {
1817 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1818 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
1819 let shared = SharedState::new(4);
1820 let policy = Arc::new(NetworkPolicy::default());
1821 let secrets = Arc::new(secrets);
1822 let proxy_connect = Arc::new(ProxyConnectState::new());
1823
1824 from_tx.send(Bytes::from(request)).await.unwrap();
1825 drop(from_tx);
1826
1827 TcpProxy::new(
1828 server_addr,
1829 UpstreamTcpTarget::direct(server_addr),
1830 from_rx,
1831 to_tx,
1832 Arc::new(shared),
1833 policy,
1834 secrets,
1835 None,
1836 proxy_connect,
1837 None,
1838 )
1839 .try_run()
1840 .await
1841 .unwrap();
1842
1843 handle.await.unwrap()
1844 }
1845
1846 #[tokio::test]
1847 async fn plain_http_substitutes_placeholder_when_host_arrives_in_second_segment() {
1848 let (addr, sink) = spawn_sink().await;
1851 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
1852
1853 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1854 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
1855 let proxy_connect = Arc::new(ProxyConnectState::new());
1856
1857 from_tx
1858 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
1859 .await
1860 .unwrap();
1861 from_tx
1862 .send(Bytes::from_static(
1863 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
1864 ))
1865 .await
1866 .unwrap();
1867 drop(from_tx);
1868
1869 TcpProxy::new(
1870 addr,
1871 UpstreamTcpTarget::direct(addr),
1872 from_rx,
1873 to_tx,
1874 Arc::new(SharedState::new(4)),
1875 Arc::new(NetworkPolicy::default()),
1876 Arc::new(secrets),
1877 None,
1878 proxy_connect,
1879 None,
1880 )
1881 .try_run()
1882 .await
1883 .unwrap();
1884
1885 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
1886 assert!(wire.contains("real-secret-value"), "got: {wire:?}");
1887 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
1888 }
1889
1890 #[tokio::test]
1891 async fn plain_http_forwards_placeholder_to_allowed_host_with_split_headers() {
1892 let (addr, sink) = spawn_sink().await;
1897
1898 let shared = SharedState::new(4);
1899 shared.cache_resolved_hostname(
1900 "example.com",
1901 ResolvedHostnameFamily::Ipv4,
1902 ["127.0.0.1".parse::<IpAddr>().unwrap()],
1903 StdDuration::from_secs(60),
1904 );
1905
1906 let secrets = SecretsConfig {
1907 secrets: vec![SecretEntry {
1908 env_var: "API_KEY".into(),
1909 value: zeroize::Zeroizing::new("real-secret-value".into()),
1910 source: None,
1911 placeholder: "$MSB_KEY".into(),
1912 allowed_hosts: vec![HostPattern::Exact("example.com".into())],
1913 injection: SecretInjection {
1914 headers: true,
1915 basic_auth: false,
1916 query_params: false,
1917 body: false,
1918 },
1919 on_violation: None,
1920 require_tls_identity: true,
1921 }],
1922 ..Default::default()
1923 };
1924
1925 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1926 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
1927 let proxy_connect = Arc::new(ProxyConnectState::new());
1928
1929 from_tx
1930 .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
1931 .await
1932 .unwrap();
1933 from_tx
1934 .send(Bytes::from_static(
1935 b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
1936 ))
1937 .await
1938 .unwrap();
1939 drop(from_tx);
1940
1941 TcpProxy::new(
1942 addr,
1943 UpstreamTcpTarget::direct(addr),
1944 from_rx,
1945 to_tx,
1946 Arc::new(shared),
1947 Arc::new(NetworkPolicy::default()),
1948 Arc::new(secrets),
1949 None,
1950 proxy_connect,
1951 None,
1952 )
1953 .try_run()
1954 .await
1955 .unwrap();
1956
1957 let wire = String::from_utf8(sink.await.unwrap()).unwrap();
1958 assert!(
1959 wire.contains("Host: example.com"),
1960 "request must reach the allowed host, got: {wire:?}"
1961 );
1962 assert!(
1963 wire.contains("$MSB_KEY"),
1964 "placeholder must be forwarded unchanged for a require_tls_identity secret, got: {wire:?}"
1965 );
1966 assert!(
1967 !wire.contains("real-secret-value"),
1968 "secret must never be substituted over plain HTTP, got: {wire:?}"
1969 );
1970 }
1971
1972 #[tokio::test]
1973 async fn plain_http_substitutes_placeholder_in_first_flight() {
1974 let (addr, sink) = spawn_sink().await;
1975
1976 let request =
1977 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
1978 .to_vec();
1979 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
1980
1981 let wire =
1982 String::from_utf8(relay_through_proxy(request, secrets, sink, addr).await).unwrap();
1983 assert!(
1984 wire.contains("real-secret-value"),
1985 "real value must reach server, got: {wire:?}"
1986 );
1987 assert!(
1988 !wire.contains("$MSB_KEY"),
1989 "placeholder must not reach server, got: {wire:?}"
1990 );
1991 }
1992
1993 #[tokio::test]
1994 async fn plain_http_no_substitution_when_require_tls_identity_true() {
1995 let (addr, sink) = spawn_sink().await;
1996
1997 let request =
1998 b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
1999 .to_vec();
2000 let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", true);
2001
2002 let wire =
2003 String::from_utf8_lossy(&relay_through_proxy(request, secrets, sink, addr).await)
2004 .into_owned();
2005 assert!(
2006 wire.contains("$MSB_KEY"),
2007 "placeholder must be forwarded unchanged when require_tls_identity=true, got: {wire:?}"
2008 );
2009 assert!(
2010 !wire.contains("real-secret-value"),
2011 "real value must not leak when require_tls_identity=true, got: {wire:?}"
2012 );
2013 }
2014
2015 #[tokio::test]
2016 async fn plain_http_large_body_forwarded_verbatim_in_relay_loop() {
2017 let (addr, sink) = spawn_sink().await;
2021 let secrets = make_plain_http_secret("$MSB_KEY", "real-value", false);
2022
2023 let body = "x".repeat(32_000);
2024 let header = format!(
2025 "POST /upload HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\nContent-Length: {}\r\n\r\n",
2026 body.len()
2027 );
2028
2029 let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2030 let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2031 let proxy_connect = Arc::new(ProxyConnectState::new());
2032
2033 from_tx
2034 .send(Bytes::from(header.into_bytes()))
2035 .await
2036 .unwrap();
2037 from_tx
2038 .send(Bytes::from(body.clone().into_bytes()))
2039 .await
2040 .unwrap();
2041 drop(from_tx);
2042
2043 TcpProxy::new(
2044 addr,
2045 UpstreamTcpTarget::direct(addr),
2046 from_rx,
2047 to_tx,
2048 Arc::new(SharedState::new(4)),
2049 Arc::new(NetworkPolicy::default()),
2050 Arc::new(secrets),
2051 None,
2052 proxy_connect,
2053 None,
2054 )
2055 .try_run()
2056 .await
2057 .unwrap();
2058
2059 let wire = String::from_utf8_lossy(&sink.await.unwrap()).into_owned();
2060 assert!(wire.contains(&body), "got {} bytes", wire.len());
2061 assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2062 }
2063}