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