Skip to main content

microsandbox_network/engine/tcp/
proxy.rs

1//! Bidirectional TCP proxy: smoltcp socket ↔ channels ↔ tokio socket.
2//!
3//! Each outbound guest TCP connection gets a proxy task that opens a real
4//! TCP connection to the destination via tokio and relays data between the
5//! channel pair (connected to the smoltcp socket in the poll loop) and the
6//! real server.
7
8use std::borrow::Cow;
9use std::io;
10use std::net::{IpAddr, SocketAddr};
11use std::sync::Arc;
12use std::time::Duration;
13
14use bytes::Bytes;
15use tokio::io::{AsyncReadExt, AsyncWriteExt};
16use tokio::net::TcpStream;
17use tokio::sync::mpsc;
18
19use super::connection::ProxyConnectState;
20#[cfg(test)]
21use super::connection::ProxyConnectStatus;
22use super::upstream::UpstreamTcpTarget;
23use crate::engine::secrets::config::SecretsConfigExt;
24use crate::engine::tls::proxy::TlsProxy;
25use crate::engine::tls::sni;
26use crate::engine::tls::state::TlsState;
27use crate::netstack::shared::SharedState;
28use crate::policy::{EgressEvaluation, HostnameSource, NetworkPolicy, Protocol};
29use crate::proxy::ResolvedOutboundProxy;
30use crate::secrets::config::{SecretViolationAction, SecretsConfig};
31use crate::secrets::handler::{
32    SecretsHandler, first_line_is_not_http_request, looks_like_http_request_prefix,
33};
34
35//--------------------------------------------------------------------------------------------------
36// Constants
37//--------------------------------------------------------------------------------------------------
38
39/// Buffer size for reading from the real server.
40const SERVER_READ_BUF_SIZE: usize = 16384;
41
42/// Max bytes buffered while reading the proxy's CONNECT response headers.
43const CONNECT_RESP_LIMIT: usize = 8192;
44
45/// Max bytes to buffer while peeking for the ClientHello's SNI.
46const PEEK_BUF_SIZE: usize = 16384;
47
48/// Upper bound on time spent buffering the first flight before
49/// falling back to a cache-only egress decision.
50const PEEK_BUDGET: Duration = Duration::from_secs(5);
51
52//--------------------------------------------------------------------------------------------------
53// Types
54//--------------------------------------------------------------------------------------------------
55
56#[derive(Debug)]
57struct ConnectRequest {
58    bytes: Vec<u8>,
59    header_end: usize,
60    target: ConnectTarget,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64struct ConnectTarget {
65    host: String,
66    port: u16,
67    expected_sni: Option<String>,
68}
69
70/// Per-connection TCP proxy task and the state it owns.
71pub(crate) struct TcpProxy {
72    guest_dst: SocketAddr,
73    connect_target: UpstreamTcpTarget,
74    from_smoltcp: mpsc::Receiver<Bytes>,
75    to_smoltcp: mpsc::Sender<Bytes>,
76    shared: Arc<SharedState>,
77    network_policy: Arc<NetworkPolicy>,
78    secrets: Arc<SecretsConfig>,
79    tls_state: Option<Arc<TlsState>>,
80    strict: bool,
81    proxy_connect: Arc<ProxyConnectState>,
82    outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
83}
84
85//--------------------------------------------------------------------------------------------------
86// Methods
87//--------------------------------------------------------------------------------------------------
88
89impl ConnectRequest {
90    fn header_bytes(&self) -> &[u8] {
91        &self.bytes[..self.header_end]
92    }
93
94    fn post_header_bytes(&self) -> &[u8] {
95        &self.bytes[self.header_end..]
96    }
97}
98
99impl ConnectTarget {
100    fn is_intercepted(&self, tls_state: &TlsState) -> bool {
101        tls_state.config.intercepted_ports.contains(&self.port)
102    }
103
104    fn guest_dst(&self, fallback: SocketAddr, shared: &SharedState) -> SocketAddr {
105        if let Ok(ip) = self.host.parse::<IpAddr>() {
106            return SocketAddr::new(ip, self.port);
107        }
108
109        if self.host.eq_ignore_ascii_case(crate::HOST_ALIAS) {
110            match fallback.ip() {
111                IpAddr::V4(_) => {
112                    if let Some(ip) = shared.gateway_ipv4() {
113                        return SocketAddr::new(IpAddr::V4(ip), self.port);
114                    }
115                }
116                IpAddr::V6(_) => {
117                    if let Some(ip) = shared.gateway_ipv6() {
118                        return SocketAddr::new(IpAddr::V6(ip), self.port);
119                    }
120                }
121            }
122            if let Some(ip) = shared.gateway_ipv4() {
123                return SocketAddr::new(IpAddr::V4(ip), self.port);
124            }
125            if let Some(ip) = shared.gateway_ipv6() {
126                return SocketAddr::new(IpAddr::V6(ip), self.port);
127            }
128        }
129
130        SocketAddr::new(fallback.ip(), self.port)
131    }
132}
133
134impl TcpProxy {
135    /// Build a proxy for a newly established guest TCP connection.
136    #[allow(clippy::too_many_arguments)]
137    pub(crate) fn new(
138        guest_dst: SocketAddr,
139        connect_target: UpstreamTcpTarget,
140        from_smoltcp: mpsc::Receiver<Bytes>,
141        to_smoltcp: mpsc::Sender<Bytes>,
142        shared: Arc<SharedState>,
143        network_policy: Arc<NetworkPolicy>,
144        secrets: Arc<SecretsConfig>,
145        tls_state: Option<Arc<TlsState>>,
146        strict: bool,
147        proxy_connect: Arc<ProxyConnectState>,
148        outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
149    ) -> Self {
150        Self {
151            guest_dst,
152            connect_target,
153            from_smoltcp,
154            to_smoltcp,
155            shared,
156            network_policy,
157            secrets,
158            tls_state,
159            strict,
160            proxy_connect,
161            outbound_proxy,
162        }
163    }
164
165    /// Run the TCP proxy task to completion.
166    pub(crate) async fn run(self) {
167        let guest_dst = self.guest_dst;
168        let connect_dst = self.connect_target.primary();
169
170        if let Err(error) = self.try_run().await {
171            tracing::debug!(
172                dst = %connect_dst,
173                %guest_dst,
174                %error,
175                "TCP proxy task ended",
176            );
177        }
178    }
179
180    /// Drive the TCP proxy to completion, returning operational failures.
181    async fn try_run(self) -> io::Result<()> {
182        let Self {
183            guest_dst,
184            connect_target,
185            mut from_smoltcp,
186            to_smoltcp,
187            shared,
188            network_policy,
189            secrets,
190            tls_state,
191            strict,
192            proxy_connect,
193            outbound_proxy,
194        } = self;
195
196        // Mirror the SYN-time policy walk so only flows that actually reached a
197        // hostname rule wait for client bytes. A domain rule elsewhere in the
198        // policy must not stall unrelated or server-first traffic.
199        let hostname_policy_deferred = match network_policy.evaluate_egress_with_source(
200            guest_dst,
201            Protocol::Tcp,
202            &shared,
203            HostnameSource::Deferred,
204        ) {
205            EgressEvaluation::Allow => false,
206            EgressEvaluation::DeferUntilHostname => true,
207            // Preserve the existing fail-closed path if a DNS-cache binding
208            // expires between the SYN evaluation and proxy startup.
209            EgressEvaluation::Deny => network_policy.has_domain_rules(),
210        };
211
212        // Pre-connect peek is only for domain policy: the hostname has to be known
213        // before we dial upstream so a Deny never opens a connection. Secrets do
214        // *not* gate the connect, so they no longer force a peek here — that work is
215        // deferred to `classify_first_flight` after the socket is open, where it can
216        // run without stalling server-first protocols (see below).
217        let (initial_buf, sni) = if hostname_policy_deferred {
218            peek_for_sni(&mut from_smoltcp, PEEK_BUF_SIZE, PEEK_BUDGET).await
219        } else {
220            (Vec::new(), None)
221        };
222
223        // Re-evaluate egress against the *guest* dst — the address the
224        // guest dialed, not the post-rewrite host-side address. SNI
225        // refines over-allow when the cache matched a shared CDN IP;
226        // CacheOnly is the non-TLS fallback path so Domain rules still
227        // gate plain HTTP / SSH / etc.
228        if hostname_policy_deferred {
229            let source = match sni.as_deref() {
230                Some(name) => HostnameSource::Sni(name),
231                None => HostnameSource::CacheOnly,
232            };
233            match network_policy.evaluate_egress_with_source(
234                guest_dst,
235                Protocol::Tcp,
236                &shared,
237                source,
238            ) {
239                EgressEvaluation::Allow => {
240                    if strict_hostname_allow_is_opaque(
241                        strict,
242                        &network_policy,
243                        guest_dst,
244                        &shared,
245                        sni.as_deref(),
246                        &initial_buf,
247                    ) {
248                        tracing::debug!(
249                            sni = sni.as_deref(),
250                            dst = %guest_dst,
251                            "TCP egress denied by strict hostname policy",
252                        );
253                        proxy_connect.mark_policy_denied();
254                        shared.proxy_wake.wake();
255                        return Ok(());
256                    }
257                }
258                EgressEvaluation::Deny => {
259                    tracing::debug!(
260                        dst = %guest_dst,
261                        source = source.label(),
262                        "TCP egress denied by domain policy",
263                    );
264                    proxy_connect.mark_policy_denied();
265                    shared.proxy_wake.wake();
266                    return Ok(());
267                }
268                EgressEvaluation::DeferUntilHostname => {
269                    debug_assert!(false, "DeferUntilHostname leaked into TCP proxy task");
270                    proxy_connect.mark_policy_denied();
271                    shared.proxy_wake.wake();
272                    return Ok(());
273                }
274            }
275        }
276
277        // A policy-required peek may already have captured a CONNECT request.
278        // Otherwise the post-connect paths below classify it without delaying
279        // server-first protocols.
280        if let Some(tls_state) = tls_state.clone()
281            && !initial_buf.is_empty()
282            && could_be_connect_request(&initial_buf)
283        {
284            return handle_connect_tunnel(
285                guest_dst,
286                connect_target,
287                initial_buf,
288                from_smoltcp,
289                to_smoltcp,
290                shared,
291                network_policy,
292                tls_state,
293                strict,
294                proxy_connect,
295                outbound_proxy,
296                None,
297            )
298            .await;
299        }
300
301        // Connect upstream *before* finishing the secrets-side classification. A
302        // server-first protocol (SSH, SMTP, a database) sends nothing until it has
303        // seen the server's banner; with the socket already open we can relay that
304        // banner while we wait, instead of burning the peek budget pre-connect.
305        let stream = connect_target
306            .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
307            .await?;
308        let connect_dst = stream.peer_addr().unwrap_or(connect_target.primary());
309        let (mut server_rx, mut server_tx) = stream.into_split();
310
311        // Finish classifying the first flight (TLS vs plain HTTP) and, for
312        // plain-HTTP candidates, gather a full header block — without blocking the
313        // server→guest direction. When domain rules already peeked, `initial_buf`
314        // is reused and this is cheap; with no secrets it is skipped entirely
315        // (`is_tls` only matters for deciding whether to build the handler).
316        let enforce_http_authority = network_policy.has_domain_rules();
317        let want_headers = enforce_http_authority
318            || secrets.has_plain_http_candidates()
319            || secrets.has_host_scoped_secrets();
320        let (initial_buf, is_tls) = if want_headers {
321            classify_first_flight(
322                initial_buf,
323                &mut from_smoltcp,
324                &mut server_rx,
325                &to_smoltcp,
326                &shared,
327                want_headers,
328                PEEK_BUF_SIZE,
329                PEEK_BUDGET,
330            )
331            .await?
332        } else {
333            (initial_buf, false)
334        };
335
336        if let Some(tls_state) = tls_state.clone()
337            && could_be_connect_request(&initial_buf)
338        {
339            // A policy-required peek can miss a client whose first bytes arrive
340            // after we dial upstream. Once classify_first_flight has captured the
341            // request, rejoin the already-open proxy socket and use the CONNECT path
342            // so intercepted tunnels still get TLS substitution and policy checks.
343            let proxy_stream = server_rx
344                .reunite(server_tx)
345                .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
346            return handle_connect_tunnel(
347                guest_dst,
348                connect_target,
349                initial_buf,
350                from_smoltcp,
351                to_smoltcp,
352                shared,
353                network_policy,
354                tls_state,
355                strict,
356                proxy_connect,
357                outbound_proxy,
358                Some(proxy_stream),
359            )
360            .await;
361        }
362
363        let mut late_connect_state = tls_state;
364        let mut secrets_handler: Option<SecretsHandler> = if is_tls {
365            None
366        } else if enforce_http_authority {
367            let host = extract_http_host(&initial_buf).unwrap_or_default();
368            Some(SecretsHandler::new_plain_http_policy(
369                &secrets,
370                &host,
371                guest_dst,
372                network_policy.clone(),
373                shared.clone(),
374            ))
375        } else if !secrets.secrets.is_empty() {
376            Some(match extract_http_host(&initial_buf) {
377                Some(host) => {
378                    SecretsHandler::new_plain_http(&secrets, &host, guest_dst.ip(), &shared)
379                }
380                None => SecretsHandler::new_plain_http_invalid_host(&secrets),
381            })
382        } else {
383            None
384        };
385
386        // Replay the buffered first flight — run through secrets handler first.
387        if !initial_buf.is_empty() {
388            let out: Cow<[u8]> = match secrets_handler.as_mut() {
389                Some(h) => match h.substitute(&initial_buf) {
390                    // Borrow the input when nothing was substituted; only a chunk
391                    // that actually carries a placeholder is reallocated.
392                    Ok(cow) => cow,
393                    Err(action) => {
394                        tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation in first flight");
395                        if matches!(action, SecretViolationAction::BlockAndTerminate) {
396                            shared.trigger_termination();
397                        }
398                        return Ok(());
399                    }
400                },
401                None => Cow::Borrowed(&initial_buf),
402            };
403            if !out.is_empty() {
404                if let Err(e) = server_tx.write_all(&out).await {
405                    tracing::debug!(dst = %connect_dst, error = %e, "replay of buffered first flight failed");
406                    return Ok(());
407                }
408                if let Err(e) = server_tx.flush().await {
409                    tracing::debug!(dst = %connect_dst, error = %e, "flush after first flight failed");
410                    return Ok(());
411                }
412            }
413        }
414
415        let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
416
417        // Bidirectional relay using tokio::select!.
418        //
419        // guest → server: receive from channel, write to server socket.
420        // server → guest: read from server socket, send via channel + wake poll.
421        let mut guest_eof = false;
422        loop {
423            tokio::select! {
424                // Guest → server: substitute placeholders before forwarding.
425                data = from_smoltcp.recv(), if !guest_eof => {
426                    match data {
427                        Some(bytes) => {
428                            if let Some(tls_state) = late_connect_state.take()
429                                && could_be_connect_request(&bytes)
430                            {
431                                // The first guest bytes can arrive after both peek
432                                // windows have completed. Nothing has been written
433                                // to the proxy socket yet, so this is still a valid
434                                // point to switch into CONNECT tunnel handling.
435                                let proxy_stream = server_rx
436                                    .reunite(server_tx)
437                                    .map_err(|_| io::Error::other("failed to reunite proxy stream halves"))?;
438                                return handle_connect_tunnel(
439                                    guest_dst,
440                                    connect_target,
441                                    bytes.to_vec(),
442                                    from_smoltcp,
443                                    to_smoltcp,
444                                    shared,
445                                    network_policy,
446                                    tls_state,
447                                    strict,
448                                    proxy_connect,
449                                    outbound_proxy,
450                                    Some(proxy_stream),
451                                )
452                                .await;
453                            }
454                            // No handler (no secrets / TLS) is the common path: forward
455                            // the chunk borrowed, with no per-chunk allocation or copy.
456                            let out: Cow<[u8]> = match secrets_handler.as_mut() {
457                                Some(h) => match h.substitute(&bytes) {
458                                    Ok(cow) => cow,
459                                    Err(action) => {
460                                        tracing::warn!(dst = %connect_dst, violation = ?action, "secret violation");
461                                        if matches!(action, SecretViolationAction::BlockAndTerminate)
462                                        {
463                                            shared.trigger_termination();
464                                        }
465                                        break;
466                                    }
467                                },
468                                None => Cow::Borrowed(&bytes),
469                            };
470                            if !out.is_empty() {
471                                if let Err(e) = server_tx.write_all(&out).await {
472                                    tracing::debug!(dst = %connect_dst, error = %e, "write to server failed");
473                                    break;
474                                }
475                                if let Err(e) = server_tx.flush().await {
476                                    tracing::debug!(dst = %connect_dst, error = %e, "flush to server failed");
477                                    break;
478                                }
479                            }
480                        }
481                        // Channel closed — the guest half-closed (FIN) or the
482                        // connection was torn down. Propagate the half-close:
483                        // stop sending upstream but keep relaying server →
484                        // guest until the server closes.
485                        None => {
486                            guest_eof = true;
487                            if server_tx.shutdown().await.is_err() {
488                                break;
489                            }
490                        }
491                    }
492                }
493
494                // Server → guest: no substitution — server never sends placeholders.
495                result = server_rx.read(&mut server_buf) => {
496                    match result {
497                        Ok(0) => break, // Server closed connection.
498                        Ok(n) => {
499                            // A server-first byte means this is not an HTTP CONNECT
500                            // tunnel to a proxy. Keep relaying normally afterward.
501                            late_connect_state = None;
502                            let data = Bytes::copy_from_slice(&server_buf[..n]);
503                            if to_smoltcp.send(data).await.is_err() {
504                                // Channel closed — poll loop dropped the receiver.
505                                break;
506                            }
507                            // Wake the poll thread so it writes data to the
508                            // smoltcp socket.
509                            shared.proxy_wake.wake();
510                        }
511                        Err(e) => {
512                            tracing::debug!(dst = %connect_dst, error = %e, "read from server failed");
513                            break;
514                        }
515                    }
516                }
517            }
518        }
519
520        Ok(())
521    }
522}
523
524//--------------------------------------------------------------------------------------------------
525// Functions
526//--------------------------------------------------------------------------------------------------
527
528/// Spawn a TCP proxy task for a newly established connection.
529///
530/// `guest_dst` is what the guest dialed — the address policy rules match
531/// against. `connect_dst` is the host-side address tokio actually dials.
532///
533/// `proxy_connect` is updated before the task exits so the connection
534/// tracker can decide between FIN (clean close) and RST (upstream
535/// connect failure).
536#[allow(clippy::too_many_arguments)]
537pub fn spawn_tcp_proxy(
538    handle: &tokio::runtime::Handle,
539    guest_dst: SocketAddr,
540    connect_dst: SocketAddr,
541    from_smoltcp: mpsc::Receiver<Bytes>,
542    to_smoltcp: mpsc::Sender<Bytes>,
543    shared: Arc<SharedState>,
544    network_policy: Arc<NetworkPolicy>,
545    secrets: Arc<SecretsConfig>,
546    tls_state: Option<Arc<TlsState>>,
547    strict: bool,
548    proxy_connect: Arc<ProxyConnectState>,
549    outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
550) {
551    let proxy = TcpProxy::new(
552        guest_dst,
553        UpstreamTcpTarget::direct(connect_dst),
554        from_smoltcp,
555        to_smoltcp,
556        shared,
557        network_policy,
558        secrets,
559        tls_state,
560        strict,
561        proxy_connect,
562        outbound_proxy,
563    );
564
565    handle.spawn(proxy.run());
566}
567
568fn strict_hostname_allow_is_opaque(
569    strict: bool,
570    network_policy: &NetworkPolicy,
571    guest_dst: SocketAddr,
572    shared: &SharedState,
573    sni: Option<&str>,
574    initial_buf: &[u8],
575) -> bool {
576    if !strict {
577        return false;
578    }
579
580    let source = if let Some(name) = sni {
581        HostnameSource::Sni(name)
582    } else if initial_buf.is_empty() || initial_buf.first() == Some(&0x16) {
583        HostnameSource::CacheOnly
584    } else {
585        return false;
586    };
587
588    network_policy.allows_egress_via_hostname(guest_dst, Protocol::Tcp, shared, source)
589}
590
591/// Forward an HTTP CONNECT tunnel: dial the proxy, splice the handshake,
592/// then hand the established stream to [`TlsProxy`] for TLS MITM.
593///
594/// `guest_dst` is what the guest dialed; `proxy_target` contains the rewritten
595/// loopback address the gateway actually connects to and its optional fallback.
596#[allow(clippy::too_many_arguments)]
597async fn handle_connect_tunnel(
598    guest_dst: SocketAddr,
599    proxy_target: UpstreamTcpTarget,
600    initial_buf: Vec<u8>,
601    mut from_smoltcp: mpsc::Receiver<Bytes>,
602    to_smoltcp: mpsc::Sender<Bytes>,
603    shared: Arc<SharedState>,
604    network_policy: Arc<NetworkPolicy>,
605    tls_state: Arc<TlsState>,
606    strict: bool,
607    proxy_connect: Arc<ProxyConnectState>,
608    outbound_proxy: Option<Arc<ResolvedOutboundProxy>>,
609    preconnected_proxy: Option<TcpStream>,
610) -> io::Result<()> {
611    let proxy_dst = proxy_target.primary();
612    let connect_req =
613        parse_connect_request(buffer_connect_request(initial_buf, &mut from_smoltcp).await?)?;
614
615    let connect_headers = match sanitize_connect_headers(
616        connect_req.header_bytes(),
617        &tls_state.secrets.load(),
618    ) {
619        Ok(headers) => headers,
620        Err(action) => {
621            tracing::warn!(dst = %proxy_dst, violation = ?action, "secret violation in CONNECT headers");
622            if matches!(action, SecretViolationAction::BlockAndTerminate) {
623                shared.trigger_termination();
624            }
625            return Ok(());
626        }
627    };
628
629    // Dial the proxy and forward the CONNECT request so it opens the tunnel.
630    let mut proxy_stream = match preconnected_proxy {
631        Some(stream) => stream,
632        None => {
633            proxy_target
634                .connect(&proxy_connect, &shared, outbound_proxy.as_deref())
635                .await?
636        }
637    };
638
639    if !connect_req.target.is_intercepted(&tls_state) {
640        let tunnel_dst = connect_req.target.guest_dst(guest_dst, &shared);
641        if strict
642            && let Some(expected_sni) = connect_req.target.expected_sni.as_deref()
643            && network_policy.allows_egress_via_hostname(
644                tunnel_dst,
645                Protocol::Tcp,
646                &shared,
647                HostnameSource::Sni(expected_sni),
648            )
649        {
650            tracing::debug!(
651                sni = %expected_sni,
652                dst = %tunnel_dst,
653                "CONNECT tunnel denied by strict hostname policy",
654            );
655            proxy_connect.mark_policy_denied();
656            shared.proxy_wake.wake();
657            return Ok(());
658        }
659        proxy_stream.write_all(&connect_headers).await?;
660        proxy_stream.flush().await?;
661        let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
662        if to_smoltcp
663            .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
664            .await
665            .is_err()
666        {
667            return Ok(());
668        }
669        if !proxy_resp[header_end..].is_empty()
670            && to_smoltcp
671                .send(Bytes::copy_from_slice(&proxy_resp[header_end..]))
672                .await
673                .is_err()
674        {
675            return Ok(());
676        }
677        shared.proxy_wake.wake();
678        if !connect_response_is_success(&proxy_resp[..header_end]) {
679            proxy_connect.mark_connected();
680            return Ok(());
681        }
682        if !connect_req.post_header_bytes().is_empty() {
683            proxy_stream
684                .write_all(connect_req.post_header_bytes())
685                .await?;
686        }
687        proxy_stream.flush().await?;
688        proxy_connect.mark_connected();
689        return relay_connected_stream(proxy_stream, from_smoltcp, to_smoltcp, shared).await;
690    }
691
692    proxy_stream.write_all(&connect_headers).await?;
693    proxy_stream.flush().await?;
694
695    let (proxy_resp, header_end) = read_connect_response_headers(&mut proxy_stream).await?;
696    if !connect_response_is_success(&proxy_resp[..header_end]) {
697        return Err(io::Error::new(
698            io::ErrorKind::ConnectionRefused,
699            "proxy rejected CONNECT",
700        ));
701    }
702    if !proxy_resp[header_end..].is_empty() {
703        return Err(io::Error::new(
704            io::ErrorKind::InvalidData,
705            "proxy sent unexpected bytes after CONNECT response headers",
706        ));
707    }
708    proxy_connect.mark_connected();
709
710    if to_smoltcp
711        .send(Bytes::copy_from_slice(&proxy_resp[..header_end]))
712        .await
713        .is_err()
714    {
715        return Ok(());
716    }
717    shared.proxy_wake.wake();
718
719    let tls_seed = connect_req.post_header_bytes().to_vec();
720    let tls_guest_dst = connect_req.target.guest_dst(guest_dst, &shared);
721    let expected_sni = connect_req.target.expected_sni.clone();
722
723    TlsProxy::new(
724        tls_guest_dst,
725        proxy_target,
726        from_smoltcp,
727        to_smoltcp,
728        shared,
729        tls_state,
730        network_policy,
731        strict,
732        proxy_connect,
733        // Unused: `upstream_stream` is already `Some` below, so the
734        // outbound proxy (already applied when dialing `proxy_stream`
735        // above) is never consulted again.
736        None,
737    )
738    .with_upstream(proxy_stream)
739    .with_expected_sni(expected_sni)
740    .with_initial_buf(tls_seed)
741    .try_run()
742    .await
743}
744
745/// Relay an established TCP stream without inspecting or substituting bytes.
746async fn relay_connected_stream(
747    stream: TcpStream,
748    mut from_smoltcp: mpsc::Receiver<Bytes>,
749    to_smoltcp: mpsc::Sender<Bytes>,
750    shared: Arc<SharedState>,
751) -> io::Result<()> {
752    let (mut server_rx, mut server_tx) = stream.into_split();
753    let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
754
755    let mut guest_eof = false;
756    loop {
757        tokio::select! {
758            data = from_smoltcp.recv(), if !guest_eof => {
759                match data {
760                    Some(bytes) => {
761                        server_tx.write_all(&bytes).await?;
762                        server_tx.flush().await?;
763                    }
764                    // Guest half-closed (FIN): stop sending upstream but
765                    // keep relaying server → guest until the server closes.
766                    None => {
767                        guest_eof = true;
768                        if server_tx.shutdown().await.is_err() {
769                            break;
770                        }
771                    }
772                }
773            }
774            result = server_rx.read(&mut server_buf) => {
775                match result {
776                    Ok(0) => break,
777                    Ok(n) => {
778                        if to_smoltcp
779                            .send(Bytes::copy_from_slice(&server_buf[..n]))
780                            .await
781                            .is_err()
782                        {
783                            break;
784                        }
785                        shared.proxy_wake.wake();
786                    }
787                    Err(e) => return Err(e),
788                }
789            }
790        }
791    }
792
793    Ok(())
794}
795
796async fn buffer_connect_request(
797    mut buf: Vec<u8>,
798    from_smoltcp: &mut mpsc::Receiver<Bytes>,
799) -> io::Result<Vec<u8>> {
800    let timeout_fut = tokio::time::sleep(PEEK_BUDGET);
801    tokio::pin!(timeout_fut);
802
803    loop {
804        if !could_be_connect_request(&buf) {
805            return Err(io::Error::new(
806                io::ErrorKind::InvalidData,
807                "malformed CONNECT request prefix",
808            ));
809        }
810        if headers_end(&buf).is_some() {
811            return Ok(buf);
812        }
813        if buf.len() >= PEEK_BUF_SIZE {
814            return Err(io::Error::new(
815                io::ErrorKind::InvalidData,
816                "CONNECT request headers too large",
817            ));
818        }
819
820        tokio::select! {
821            biased;
822            _ = &mut timeout_fut => {
823                return Err(io::Error::new(
824                    io::ErrorKind::TimedOut,
825                    "timed out waiting for complete CONNECT request headers",
826                ));
827            }
828            data = from_smoltcp.recv() => match data {
829                Some(bytes) => {
830                    buf.extend_from_slice(&bytes);
831                }
832                None => {
833                    return Err(io::Error::new(
834                        io::ErrorKind::UnexpectedEof,
835                        "channel closed before complete CONNECT request headers",
836                    ));
837                }
838            }
839        }
840    }
841}
842
843async fn read_connect_response_headers(stream: &mut TcpStream) -> io::Result<(Vec<u8>, usize)> {
844    tokio::time::timeout(PEEK_BUDGET, async {
845        let mut proxy_resp = Vec::with_capacity(256);
846        let mut buf = [0u8; 4096];
847        loop {
848            let n = stream.read(&mut buf).await?;
849            if n == 0 {
850                return Err(io::Error::new(
851                    io::ErrorKind::UnexpectedEof,
852                    "proxy closed before sending CONNECT response",
853                ));
854            }
855            proxy_resp.extend_from_slice(&buf[..n]);
856            if let Some(end) = headers_end(&proxy_resp) {
857                return Ok((proxy_resp, end));
858            }
859            if proxy_resp.len() > CONNECT_RESP_LIMIT {
860                return Err(io::Error::new(
861                    io::ErrorKind::InvalidData,
862                    "proxy CONNECT response too large",
863                ));
864            }
865        }
866    })
867    .await
868    .map_err(|_| {
869        io::Error::new(
870            io::ErrorKind::TimedOut,
871            "timed out waiting for proxy CONNECT response",
872        )
873    })?
874}
875
876fn sanitize_connect_headers<'a>(
877    header_bytes: &'a [u8],
878    secrets: &SecretsConfig,
879) -> Result<Cow<'a, [u8]>, SecretViolationAction> {
880    if secrets.secrets.is_empty() {
881        return Ok(Cow::Borrowed(header_bytes));
882    }
883
884    let mut handler = SecretsHandler::new_plain_http_untrusted_metadata(secrets);
885    handler.substitute(header_bytes)
886}
887
888/// Returns the byte offset just past the `\r\n\r\n` header terminator, or `None`.
889fn headers_end(buf: &[u8]) -> Option<usize> {
890    buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
891}
892
893fn could_be_connect_request(buf: &[u8]) -> bool {
894    const PREFIX: &[u8] = b"CONNECT ";
895    if buf.is_empty() {
896        return false;
897    }
898    let n = buf.len().min(PREFIX.len());
899    buf[..n].eq_ignore_ascii_case(&PREFIX[..n])
900}
901
902fn parse_connect_request(bytes: Vec<u8>) -> io::Result<ConnectRequest> {
903    let header_end = headers_end(&bytes).ok_or_else(|| {
904        io::Error::new(
905            io::ErrorKind::InvalidData,
906            "incomplete CONNECT request headers",
907        )
908    })?;
909    let target = {
910        let request_line = bytes[..header_end]
911            .split(|&b| b == b'\n')
912            .next()
913            .unwrap_or(&[]);
914        let request_line = std::str::from_utf8(request_line)
915            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "CONNECT line is not UTF-8"))?
916            .trim_end_matches('\r');
917        let mut parts = request_line.split_ascii_whitespace();
918        let method = parts.next().unwrap_or_default();
919        let authority = parts.next().unwrap_or_default();
920        let version = parts.next().unwrap_or_default();
921        if !method.eq_ignore_ascii_case("CONNECT")
922            || authority.is_empty()
923            || !is_http_version(version)
924            || parts.next().is_some()
925        {
926            return Err(io::Error::new(
927                io::ErrorKind::InvalidData,
928                "malformed CONNECT request line",
929            ));
930        }
931        parse_connect_target(authority)?
932    };
933
934    Ok(ConnectRequest {
935        bytes,
936        header_end,
937        target,
938    })
939}
940
941fn parse_connect_target(authority: &str) -> io::Result<ConnectTarget> {
942    let authority = authority.trim();
943    let (host, port) = if let Some(rest) = authority.strip_prefix('[') {
944        let (host, rest) = rest.split_once(']').ok_or_else(|| {
945            io::Error::new(
946                io::ErrorKind::InvalidData,
947                "malformed CONNECT IPv6 authority",
948            )
949        })?;
950        let port = rest.strip_prefix(':').ok_or_else(|| {
951            io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
952        })?;
953        (host, port)
954    } else {
955        let (host, port) = authority.rsplit_once(':').ok_or_else(|| {
956            io::Error::new(io::ErrorKind::InvalidData, "CONNECT authority missing port")
957        })?;
958        if host.contains(':') {
959            return Err(io::Error::new(
960                io::ErrorKind::InvalidData,
961                "CONNECT IPv6 authority must be bracketed",
962            ));
963        }
964        (host, port)
965    };
966    let host = host.trim().trim_end_matches('.');
967    if host.is_empty() {
968        return Err(io::Error::new(
969            io::ErrorKind::InvalidData,
970            "CONNECT authority missing host",
971        ));
972    }
973    let port = port
974        .parse::<u16>()
975        .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid CONNECT port"))?;
976    let expected_sni = host
977        .parse::<IpAddr>()
978        .is_err()
979        .then(|| host.to_ascii_lowercase());
980
981    Ok(ConnectTarget {
982        host: host.to_ascii_lowercase(),
983        port,
984        expected_sni,
985    })
986}
987
988fn is_http_version(version: &str) -> bool {
989    let Some(version) = version.strip_prefix("HTTP/") else {
990        return false;
991    };
992    let Some((major, minor)) = version.split_once('.') else {
993        return false;
994    };
995    !major.is_empty()
996        && !minor.is_empty()
997        && major.bytes().all(|b| b.is_ascii_digit())
998        && minor.bytes().all(|b| b.is_ascii_digit())
999}
1000
1001fn connect_response_is_success(headers: &[u8]) -> bool {
1002    let Some(status_line) = headers.split(|&b| b == b'\n').next() else {
1003        return false;
1004    };
1005    let Ok(status_line) = std::str::from_utf8(status_line) else {
1006        return false;
1007    };
1008    let mut parts = status_line.trim_end_matches('\r').split_ascii_whitespace();
1009    let version = parts.next().unwrap_or_default();
1010    let status = parts.next().unwrap_or_default();
1011    is_http_version(version)
1012        && status.len() == 3
1013        && status
1014            .parse::<u16>()
1015            .is_ok_and(|code| (200..300).contains(&code))
1016}
1017
1018/// Extract the `Host:` header value from an already-buffered HTTP header block.
1019///
1020/// Returns `None` if:
1021/// - The first byte is `0x16` (TLS — not HTTP)
1022/// - The buffer does not yet contain `\r\n\r\n` (headers incomplete)
1023/// - No `Host:` header is present
1024///
1025/// Strips port suffix, lowercases, and trims whitespace. Result is
1026/// ready for byte-equal matching against `SecretEntry::allowed_hosts`.
1027fn extract_http_host(buf: &[u8]) -> Option<String> {
1028    if buf.first() == Some(&0x16) {
1029        return None;
1030    }
1031    // Size the header pool to the buffer rather than a fixed array: a header
1032    // line is at least four bytes (`a:\r\n`), so `len / 4` always covers the
1033    // real header count, and `httparse` never reports `TooManyHeaders` (which
1034    // would make a request with many headers look hostless). The first flight
1035    // is capped at PEEK_BUF_SIZE, so this stays bounded.
1036    let mut headers = vec![httparse::EMPTY_HEADER; (buf.len() / 4).max(16)];
1037    let mut req = httparse::Request::new(&mut headers);
1038    req.parse(buf).ok()?;
1039    req.headers
1040        .iter()
1041        .find(|h| h.name.eq_ignore_ascii_case("host"))
1042        .and_then(|h| std::str::from_utf8(h.value).ok())
1043        .map(|v| {
1044            let host = v.trim();
1045            // Strip port suffix.
1046            host.rsplit_once(':')
1047                .map(|(h, _)| h)
1048                .unwrap_or(host)
1049                .to_ascii_lowercase()
1050        })
1051        .filter(|h| !h.is_empty())
1052}
1053
1054/// Finish classifying the guest's first flight after the upstream socket is
1055/// open, returning the (possibly extended) first-flight buffer and whether it
1056/// is a TLS record.
1057///
1058/// `buf` carries whatever a pre-connect domain-rule peek already captured; when
1059/// it is non-empty the TLS/plain decision is already settled and only header
1060/// top-up runs. `want_headers` is set when at least one secret can be
1061/// substituted over plain HTTP (`SecretsConfig::has_plain_http_candidates`); it
1062/// makes the peek keep reading a non-TLS flight until `\r\n\r\n` so
1063/// [`extract_http_host`] sees a complete header block.
1064///
1065/// Crucially, this relays server→guest while it waits. Server-first protocols
1066/// (SSH, SMTP, databases) send nothing until they have seen the server's
1067/// banner; draining the server side here lets the banner reach the guest
1068/// immediately, so the guest's eventual first flight — not a 5s timeout — is
1069/// what ends the peek.
1070#[allow(clippy::too_many_arguments)]
1071async fn classify_first_flight(
1072    mut buf: Vec<u8>,
1073    from_smoltcp: &mut mpsc::Receiver<Bytes>,
1074    server_rx: &mut tokio::net::tcp::OwnedReadHalf,
1075    to_smoltcp: &mpsc::Sender<Bytes>,
1076    shared: &SharedState,
1077    want_headers: bool,
1078    max: usize,
1079    budget: Duration,
1080) -> io::Result<(Vec<u8>, bool)> {
1081    let mut server_buf = vec![0u8; SERVER_READ_BUF_SIZE];
1082    let timeout_fut = tokio::time::sleep(budget);
1083    tokio::pin!(timeout_fut);
1084
1085    loop {
1086        // Stop as soon as the protocol class is known and — for plain-HTTP
1087        // candidates — a full header block has arrived. Bail the moment a
1088        // non-TLS flight stops looking like an HTTP request so non-HTTP
1089        // protocols (SSH, Postgres) aren't withheld from upstream for the
1090        // whole budget while we wait for a `\r\n\r\n` that never comes.
1091        if !buf.is_empty() {
1092            let is_tls = buf.first() == Some(&0x16);
1093            let not_http = !is_tls
1094                && (!looks_like_http_request_prefix(&buf) || first_line_is_not_http_request(&buf));
1095            let done = !want_headers
1096                || is_tls
1097                || not_http
1098                || buf.len() >= max
1099                || buf.windows(4).any(|w| w == b"\r\n\r\n");
1100            if done {
1101                return Ok((buf, is_tls));
1102            }
1103        }
1104
1105        tokio::select! {
1106            biased;
1107            _ = &mut timeout_fut => {
1108                let is_tls = buf.first() == Some(&0x16);
1109                return Ok((buf, is_tls));
1110            }
1111            // Guest → buffer (not forwarded here; the caller replays it once the
1112            // handler is built, so substitution applies to the first flight too).
1113            guest = from_smoltcp.recv() => match guest {
1114                Some(bytes) => buf.extend_from_slice(&bytes),
1115                None => {
1116                    let is_tls = buf.first() == Some(&0x16);
1117                    return Ok((buf, is_tls));
1118                }
1119            },
1120            // Server → guest: relay immediately so a server-first banner is never
1121            // held hostage by the peek.
1122            server = server_rx.read(&mut server_buf) => match server {
1123                Ok(0) => {
1124                    let is_tls = buf.first() == Some(&0x16);
1125                    return Ok((buf, is_tls));
1126                }
1127                Ok(n) => {
1128                    let data = Bytes::copy_from_slice(&server_buf[..n]);
1129                    if to_smoltcp.send(data).await.is_err() {
1130                        let is_tls = buf.first() == Some(&0x16);
1131                        return Ok((buf, is_tls));
1132                    }
1133                    shared.proxy_wake.wake();
1134                }
1135                Err(e) => return Err(e),
1136            },
1137        }
1138    }
1139}
1140
1141/// Buffer the first flight until SNI can be extracted, or until one
1142/// of the bail-out conditions hits (channel close, buffer cap,
1143/// timeout). Never errors; non-TLS / slow / malformed input all
1144/// fall through to `None`.
1145///
1146/// On hit, the SNI is canonicalized (lowercase + trim trailing dot)
1147/// for byte-equal matching against rule destinations. The returned
1148/// buffer must be replayed verbatim to upstream before the caller
1149/// starts its relay loop.
1150async fn peek_for_sni(
1151    rx: &mut mpsc::Receiver<Bytes>,
1152    max: usize,
1153    budget: Duration,
1154) -> (Vec<u8>, Option<String>) {
1155    let mut buf = Vec::with_capacity(PEEK_BUF_SIZE.min(8192));
1156    let timeout_fut = tokio::time::sleep(budget);
1157    tokio::pin!(timeout_fut);
1158
1159    let raw_sni = loop {
1160        tokio::select! {
1161            biased;
1162            _ = &mut timeout_fut => break None,
1163            data = rx.recv() => {
1164                match data {
1165                    Some(bytes) => {
1166                        buf.extend_from_slice(&bytes);
1167                        // First byte of a TLS record is the ContentType;
1168                        // 0x16 is handshake. Anything else can't be a
1169                        // ClientHello, so don't burn the full budget on
1170                        // plain HTTP / SSH / etc.
1171                        if buf.first() != Some(&0x16) {
1172                            break None;
1173                        }
1174                        if let Some(name) = sni::extract_sni(&buf) {
1175                            break Some(name);
1176                        }
1177                        if buf.len() >= max {
1178                            break None;
1179                        }
1180                    }
1181                    None => break None,
1182                }
1183            }
1184        }
1185    };
1186
1187    let canonical = raw_sni.map(|s| s.trim_end_matches('.').to_ascii_lowercase());
1188    (buf, canonical)
1189}
1190
1191//--------------------------------------------------------------------------------------------------
1192// Tests
1193//--------------------------------------------------------------------------------------------------
1194
1195#[cfg(test)]
1196mod tests {
1197    use super::*;
1198
1199    /// Synthetic TLS ClientHello carrying SNI `example.com`. Bytes
1200    /// borrowed from `tls::sni` test fixtures so the parser sees a
1201    /// well-formed record.
1202    fn synthetic_client_hello(sni: &str) -> Vec<u8> {
1203        // Minimal but valid TLS 1.2 ClientHello with one SNI entry.
1204        // Layout: record header (5) + handshake header (4) + body.
1205        let host_bytes = sni.as_bytes();
1206        let host_len = host_bytes.len() as u16;
1207        let server_name_list_len = 3 + host_len; // type(1) + len(2) + host
1208        let extension_data_len = 2 + server_name_list_len; // list-len(2) + list
1209        let extensions_total = 4 + extension_data_len; // type(2) + len(2) + data
1210
1211        let mut body = Vec::new();
1212        // Client version
1213        body.extend_from_slice(&[0x03, 0x03]);
1214        // Random (32 bytes)
1215        body.extend_from_slice(&[0u8; 32]);
1216        // Session id length + (empty)
1217        body.push(0);
1218        // Cipher suites length + one cipher
1219        body.extend_from_slice(&[0x00, 0x02, 0x00, 0x2f]);
1220        // Compression methods length + null
1221        body.extend_from_slice(&[0x01, 0x00]);
1222        // Extensions length
1223        body.extend_from_slice(&extensions_total.to_be_bytes());
1224        // SNI extension: type 0x0000
1225        body.extend_from_slice(&[0x00, 0x00]);
1226        body.extend_from_slice(&extension_data_len.to_be_bytes());
1227        body.extend_from_slice(&server_name_list_len.to_be_bytes());
1228        body.push(0x00); // host_name type
1229        body.extend_from_slice(&host_len.to_be_bytes());
1230        body.extend_from_slice(host_bytes);
1231
1232        let handshake_len = body.len() as u32;
1233        let mut hs = Vec::new();
1234        hs.push(0x01); // ClientHello
1235        hs.extend_from_slice(&handshake_len.to_be_bytes()[1..]); // 24-bit length
1236        hs.extend_from_slice(&body);
1237
1238        let record_len = hs.len() as u16;
1239        let mut record = Vec::new();
1240        record.extend_from_slice(&[0x16, 0x03, 0x01]); // Handshake, TLS 1.0
1241        record.extend_from_slice(&record_len.to_be_bytes());
1242        record.extend_from_slice(&hs);
1243
1244        record
1245    }
1246
1247    #[tokio::test]
1248    async fn connect_upstream_dials_target_directly_without_outbound_proxy() {
1249        use tokio::net::TcpListener;
1250
1251        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1252        let addr = listener.local_addr().unwrap();
1253        let accept = tokio::spawn(async move {
1254            let (mut sock, _) = listener.accept().await.unwrap();
1255            let mut buf = [0u8; 5];
1256            sock.read_exact(&mut buf).await.unwrap();
1257            assert_eq!(&buf, b"hello");
1258        });
1259
1260        let shared = SharedState::new(4);
1261        let proxy_connect = ProxyConnectState::new();
1262        let mut stream = UpstreamTcpTarget::direct(addr)
1263            .connect(&proxy_connect, &shared, None)
1264            .await
1265            .unwrap();
1266        stream.write_all(b"hello").await.unwrap();
1267
1268        accept.await.unwrap();
1269        assert!(matches!(
1270            proxy_connect.status(),
1271            ProxyConnectStatus::Connected
1272        ));
1273    }
1274
1275    #[tokio::test]
1276    async fn early_http_connect_dials_proxy_through_configured_socks5_proxy() {
1277        let _ = rustls::crypto::ring::default_provider().install_default();
1278
1279        // This is the HTTP proxy the guest originally dialed. It is never
1280        // contacted directly; the SOCKS5 request below must carry this address.
1281        let http_proxy_addr: SocketAddr = "93.184.216.34:3128".parse().unwrap();
1282        let socks_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1283        let outbound_proxy = ResolvedOutboundProxy::Socks5 {
1284            address: socks_listener.local_addr().unwrap(),
1285            credentials: None,
1286        };
1287        let socks_task = tokio::spawn(async move {
1288            let (mut client, _) = socks_listener.accept().await.unwrap();
1289
1290            let mut greeting = [0u8; 3];
1291            client.read_exact(&mut greeting).await.unwrap();
1292            assert_eq!(greeting, [0x05, 0x01, 0x00]);
1293            client.write_all(&[0x05, 0x00]).await.unwrap();
1294
1295            let mut socks_request = [0u8; 10];
1296            client.read_exact(&mut socks_request).await.unwrap();
1297            assert_eq!(socks_request[0..4], [0x05, 0x01, 0x00, 0x01]);
1298            assert_eq!(&socks_request[4..8], &[93, 184, 216, 34]);
1299            assert_eq!(
1300                u16::from_be_bytes([socks_request[8], socks_request[9]]),
1301                3128
1302            );
1303            client
1304                .write_all(&[0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])
1305                .await
1306                .unwrap();
1307
1308            let expected_connect =
1309                b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n";
1310            let mut connect_request = vec![0u8; expected_connect.len()];
1311            client.read_exact(&mut connect_request).await.unwrap();
1312            assert_eq!(&connect_request, expected_connect);
1313            client
1314                .write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n")
1315                .await
1316                .unwrap();
1317        });
1318
1319        let connect_request =
1320            b"CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n".to_vec();
1321        let (from_tx, from_rx) = mpsc::channel(1);
1322        let (to_tx, mut to_rx) = mpsc::channel(1);
1323        drop(from_tx);
1324
1325        let tls_state = Arc::new(
1326            TlsState::new(
1327                microsandbox_types::TlsConfig::default(),
1328                crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1329            )
1330            .unwrap(),
1331        );
1332        let proxy_connect = Arc::new(ProxyConnectState::new());
1333
1334        handle_connect_tunnel(
1335            http_proxy_addr,
1336            UpstreamTcpTarget::direct(http_proxy_addr),
1337            connect_request,
1338            from_rx,
1339            to_tx,
1340            Arc::new(SharedState::new(4)),
1341            Arc::new(NetworkPolicy::default()),
1342            tls_state,
1343            false,
1344            proxy_connect.clone(),
1345            Some(Arc::new(outbound_proxy)),
1346            None,
1347        )
1348        .await
1349        .unwrap();
1350
1351        let response = to_rx.recv().await.unwrap();
1352        assert_eq!(
1353            &response[..],
1354            b"HTTP/1.1 200 Connection Established\r\n\r\n"
1355        );
1356        socks_task.await.unwrap();
1357        assert!(matches!(
1358            proxy_connect.status(),
1359            ProxyConnectStatus::Connected
1360        ));
1361    }
1362
1363    #[test]
1364    fn could_be_connect_request_matches_split_prefixes_only() {
1365        assert!(could_be_connect_request(b"C"));
1366        assert!(could_be_connect_request(b"connect "));
1367        assert!(could_be_connect_request(b"CONNECT example.com:443"));
1368        assert!(!could_be_connect_request(b"CLIENT"));
1369        assert!(!could_be_connect_request(b"GET / HTTP/1.1\r\n"));
1370    }
1371
1372    #[tokio::test]
1373    async fn buffer_connect_request_reads_split_headers() {
1374        let (tx, mut rx) = mpsc::channel(4);
1375        tx.send(Bytes::from_static(b"NECT example.com:443 HTTP/1.1\r\n"))
1376            .await
1377            .unwrap();
1378        tx.send(Bytes::from_static(b"Host: example.com\r\n\r\n"))
1379            .await
1380            .unwrap();
1381        drop(tx);
1382
1383        let buffered = buffer_connect_request(b"CON".to_vec(), &mut rx)
1384            .await
1385            .unwrap();
1386        let parsed = parse_connect_request(buffered).unwrap();
1387
1388        assert_eq!(parsed.target.host, "example.com");
1389        assert_eq!(parsed.target.port, 443);
1390        assert_eq!(parsed.target.expected_sni.as_deref(), Some("example.com"));
1391        assert!(parsed.post_header_bytes().is_empty());
1392    }
1393
1394    #[test]
1395    fn parse_connect_request_preserves_post_header_tls_seed() {
1396        let mut request = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n".to_vec();
1397        request.extend_from_slice(b"\x16\x03\x01client-hello");
1398
1399        let parsed = parse_connect_request(request).unwrap();
1400
1401        assert_eq!(
1402            parsed.header_bytes(),
1403            b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com\r\n\r\n"
1404        );
1405        assert_eq!(parsed.post_header_bytes(), b"\x16\x03\x01client-hello");
1406    }
1407
1408    #[test]
1409    fn parse_connect_target_requires_authority_port() {
1410        assert!(parse_connect_target("example.com").is_err());
1411        assert!(parse_connect_target("2001:db8::1:443").is_err());
1412
1413        let target = parse_connect_target("[2001:db8::1]:8443").unwrap();
1414        assert_eq!(target.host, "2001:db8::1");
1415        assert_eq!(target.port, 8443);
1416        assert_eq!(target.expected_sni, None);
1417    }
1418
1419    #[test]
1420    fn connect_response_success_requires_exact_2xx_status_code() {
1421        assert!(connect_response_is_success(
1422            b"HTTP/1.1 200 Connection Established\r\n\r\n"
1423        ));
1424        assert!(connect_response_is_success(
1425            b"HTTP/1.1 204 Connection Established\r\n\r\n"
1426        ));
1427        assert!(!connect_response_is_success(b"HTTP/1.1 2000 Weird\r\n\r\n"));
1428        assert!(!connect_response_is_success(b"HTTP/1.1 199 Nope\r\n\r\n"));
1429        assert!(!connect_response_is_success(b"NOTHTTP 200 OK\r\n\r\n"));
1430    }
1431
1432    #[tokio::test]
1433    async fn peek_for_sni_extracts_and_canonicalizes() {
1434        let (tx, mut rx) = mpsc::channel(4);
1435        let hello = synthetic_client_hello("Example.COM");
1436        tx.send(Bytes::from(hello.clone())).await.unwrap();
1437        drop(tx); // close so peek returns even if SNI didn't satisfy
1438
1439        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1440        assert_eq!(sni.as_deref(), Some("example.com"));
1441        assert_eq!(buf, hello);
1442    }
1443
1444    #[tokio::test]
1445    async fn peek_for_sni_returns_none_on_channel_close_without_data() {
1446        let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1447        drop(tx);
1448        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1449        assert!(buf.is_empty());
1450        assert_eq!(sni, None);
1451    }
1452
1453    #[tokio::test]
1454    async fn peek_for_sni_returns_none_on_non_tls_data() {
1455        let (tx, mut rx) = mpsc::channel(4);
1456        // Plaintext HTTP request; not a TLS record so extract_sni returns None.
1457        tx.send(Bytes::from_static(
1458            b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
1459        ))
1460        .await
1461        .unwrap();
1462        drop(tx);
1463        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1464        assert!(
1465            !buf.is_empty(),
1466            "buffered bytes must be returned for replay"
1467        );
1468        assert_eq!(sni, None);
1469    }
1470
1471    #[tokio::test]
1472    async fn peek_for_sni_falls_back_on_timeout() {
1473        let (tx, mut rx) = mpsc::channel::<Bytes>(1);
1474        // Hold the sender open but send nothing — peek must time out.
1475        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, Duration::from_millis(50)).await;
1476        drop(tx);
1477        assert!(buf.is_empty());
1478        assert_eq!(sni, None);
1479    }
1480
1481    #[tokio::test]
1482    async fn peek_for_sni_caps_at_max_bytes() {
1483        let (tx, mut rx) = mpsc::channel(4);
1484        // First byte 0x16 keeps the peek collecting past the early
1485        // non-TLS bail. Padding bytes are zero so the SNI parser never
1486        // matches and the loop drives to the size cap.
1487        let mut first = vec![0u8; 8192];
1488        first[0] = 0x16;
1489        tx.send(Bytes::from(first)).await.unwrap();
1490        tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1491        tx.send(Bytes::from(vec![0u8; 8192])).await.unwrap();
1492        drop(tx);
1493
1494        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1495        assert_eq!(sni, None, "no SNI in non-TLS data");
1496        assert!(
1497            buf.len() >= PEEK_BUF_SIZE,
1498            "buffer must hit the cap before bail-out: got {}",
1499            buf.len()
1500        );
1501    }
1502
1503    #[tokio::test]
1504    async fn peek_for_sni_bails_immediately_on_non_tls_first_byte() {
1505        let (tx, mut rx) = mpsc::channel(4);
1506        // Plain HTTP request: first byte 'G' (0x47) — clearly not TLS.
1507        tx.send(Bytes::from_static(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"))
1508            .await
1509            .unwrap();
1510        drop(tx);
1511
1512        // 5-second nominal budget; assert we returned in well under
1513        // that — the early-bail must not wait for the full window.
1514        let started = std::time::Instant::now();
1515        let (buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1516        let elapsed = started.elapsed();
1517        assert_eq!(sni, None);
1518        assert!(buf.starts_with(b"GET"));
1519        assert!(
1520            elapsed < Duration::from_millis(500),
1521            "non-TLS bail must be fast: took {elapsed:?}"
1522        );
1523    }
1524
1525    //----------------------------------------------------------------------------------------------
1526    // peek_for_sni × evaluate_egress_with_source — combined integration tests
1527    //----------------------------------------------------------------------------------------------
1528
1529    use std::net::IpAddr;
1530    use std::time::Duration as StdDuration;
1531
1532    use crate::netstack::shared::{ResolvedHostnameFamily, SharedState};
1533    use crate::policy::{Action, Destination, NetworkPolicy, PortRange, Rule};
1534
1535    const SHARED_FASTLY_IP: &str = "151.101.0.223";
1536
1537    fn shared_with(host: &str, ip: &str) -> SharedState {
1538        let shared = SharedState::new(4);
1539        shared.cache_resolved_hostname(
1540            host,
1541            ResolvedHostnameFamily::Ipv4,
1542            [ip.parse::<IpAddr>().unwrap()],
1543            StdDuration::from_secs(60),
1544        );
1545        shared
1546    }
1547
1548    fn allow_https(domain: &str) -> Rule {
1549        Rule {
1550            direction: crate::policy::Direction::Egress,
1551            destination: Destination::Domain(domain.parse().unwrap()),
1552            protocols: vec![Protocol::Tcp],
1553            ports: vec![PortRange::single(443)],
1554            action: Action::Allow,
1555        }
1556    }
1557
1558    fn allow_tcp(domain: &str, port: u16) -> Rule {
1559        Rule {
1560            direction: crate::policy::Direction::Egress,
1561            destination: Destination::Domain(domain.parse().unwrap()),
1562            protocols: vec![Protocol::Tcp],
1563            ports: vec![PortRange::single(port)],
1564            action: Action::Allow,
1565        }
1566    }
1567
1568    /// Over-allow case: cache says IP X is `pypi.org` (allowed); SNI
1569    /// is `evil.com`. SNI must override the cache and deny.
1570    #[tokio::test]
1571    async fn integration_sni_overrides_cache_for_over_allow() {
1572        let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1573        let policy = NetworkPolicy {
1574            default_egress: Action::Deny,
1575            default_ingress: Action::Allow,
1576            rules: vec![allow_https("pypi.org")],
1577        };
1578        let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1579
1580        let (tx, mut rx) = mpsc::channel(4);
1581        tx.send(Bytes::from(synthetic_client_hello("evil.com")))
1582            .await
1583            .unwrap();
1584        drop(tx);
1585
1586        let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1587        assert_eq!(sni.as_deref(), Some("evil.com"));
1588        assert!(!initial_buf.is_empty());
1589
1590        let source = sni
1591            .as_deref()
1592            .map(HostnameSource::Sni)
1593            .unwrap_or(HostnameSource::CacheOnly);
1594        let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1595        assert_eq!(
1596            eval,
1597            EgressEvaluation::Deny,
1598            "SNI=evil.com must not piggy-back on the cached pypi.org match",
1599        );
1600    }
1601
1602    /// Over-block case: cache says IP X is `ads.example.com` (denied);
1603    /// SNI is `api.example.com`. SNI must override the cache and allow.
1604    #[tokio::test]
1605    async fn integration_sni_overrides_cache_for_over_block() {
1606        let shared = shared_with("ads.example.com", SHARED_FASTLY_IP);
1607        let policy = NetworkPolicy {
1608            default_egress: Action::Allow,
1609            default_ingress: Action::Allow,
1610            rules: vec![Rule::deny_egress(Destination::Domain(
1611                "ads.example.com".parse().unwrap(),
1612            ))],
1613        };
1614        let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1615
1616        let (tx, mut rx) = mpsc::channel(4);
1617        tx.send(Bytes::from(synthetic_client_hello("api.example.com")))
1618            .await
1619            .unwrap();
1620        drop(tx);
1621
1622        let (_initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1623        assert_eq!(sni.as_deref(), Some("api.example.com"));
1624
1625        let source = sni
1626            .as_deref()
1627            .map(HostnameSource::Sni)
1628            .unwrap_or(HostnameSource::CacheOnly);
1629        let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1630        assert_eq!(
1631            eval,
1632            EgressEvaluation::Allow,
1633            "SNI=api.example.com must not be caught by the deny on ads.example.com",
1634        );
1635    }
1636
1637    /// Non-TLS first-flight falls back to `CacheOnly`; the cache
1638    /// match decides.
1639    #[tokio::test]
1640    async fn integration_non_tls_falls_back_to_cache() {
1641        let shared = shared_with("pypi.org", SHARED_FASTLY_IP);
1642        let policy = NetworkPolicy {
1643            default_egress: Action::Deny,
1644            default_ingress: Action::Allow,
1645            rules: vec![allow_https("pypi.org")],
1646        };
1647        let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1648
1649        let (tx, mut rx) = mpsc::channel(4);
1650        // Plain HTTP request; not a TLS record.
1651        tx.send(Bytes::from_static(
1652            b"GET / HTTP/1.1\r\nHost: pypi.org\r\n\r\n",
1653        ))
1654        .await
1655        .unwrap();
1656        drop(tx);
1657
1658        let (initial_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1659        assert_eq!(sni, None, "non-TLS data → no SNI");
1660        assert!(
1661            !initial_buf.is_empty(),
1662            "buffered bytes must survive for replay"
1663        );
1664
1665        let source = sni
1666            .as_deref()
1667            .map(HostnameSource::Sni)
1668            .unwrap_or(HostnameSource::CacheOnly);
1669        let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1670        assert_eq!(
1671            eval,
1672            EgressEvaluation::Allow,
1673            "cache-only fallback must still allow the cached hostname's IP",
1674        );
1675    }
1676
1677    /// SNI matches a `DomainSuffix` rule with a cache binding for the
1678    /// claimed name. Genuine pre-resolved traffic passes.
1679    #[tokio::test]
1680    async fn integration_sni_matches_domain_suffix_with_cache_binding() {
1681        let shared = shared_with("files.pythonhosted.org", SHARED_FASTLY_IP);
1682        let policy = NetworkPolicy {
1683            default_egress: Action::Deny,
1684            default_ingress: Action::Allow,
1685            rules: vec![Rule {
1686                direction: crate::policy::Direction::Egress,
1687                destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1688                protocols: vec![Protocol::Tcp],
1689                ports: vec![PortRange::single(443)],
1690                action: Action::Allow,
1691            }],
1692        };
1693        let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1694
1695        let (tx, mut rx) = mpsc::channel(4);
1696        tx.send(Bytes::from(synthetic_client_hello(
1697            "files.pythonhosted.org",
1698        )))
1699        .await
1700        .unwrap();
1701        drop(tx);
1702
1703        let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1704        let source = sni
1705            .as_deref()
1706            .map(HostnameSource::Sni)
1707            .unwrap_or(HostnameSource::CacheOnly);
1708        let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1709        assert_eq!(eval, EgressEvaluation::Allow);
1710    }
1711
1712    /// Spoofed SNI on an IP with no cache binding for any matching
1713    /// name: byte-equality with the suffix passes, but no DNS lookup
1714    /// ever tied a `*.pythonhosted.org` name to the destination, so
1715    /// the AND-check fails and the connection is denied.
1716    #[tokio::test]
1717    async fn integration_sni_denies_domain_suffix_without_cache_binding() {
1718        let shared = SharedState::new(4); // empty cache
1719        let policy = NetworkPolicy {
1720            default_egress: Action::Deny,
1721            default_ingress: Action::Allow,
1722            rules: vec![Rule {
1723                direction: crate::policy::Direction::Egress,
1724                destination: Destination::DomainSuffix(".pythonhosted.org".parse().unwrap()),
1725                protocols: vec![Protocol::Tcp],
1726                ports: vec![PortRange::single(443)],
1727                action: Action::Allow,
1728            }],
1729        };
1730        let dst = SocketAddr::new(SHARED_FASTLY_IP.parse().unwrap(), 443);
1731
1732        let (tx, mut rx) = mpsc::channel(4);
1733        tx.send(Bytes::from(synthetic_client_hello(
1734            "files.pythonhosted.org",
1735        )))
1736        .await
1737        .unwrap();
1738        drop(tx);
1739
1740        let (_buf, sni) = peek_for_sni(&mut rx, PEEK_BUF_SIZE, PEEK_BUDGET).await;
1741        let source = sni
1742            .as_deref()
1743            .map(HostnameSource::Sni)
1744            .unwrap_or(HostnameSource::CacheOnly);
1745        let eval = policy.evaluate_egress_with_source(dst, Protocol::Tcp, &shared, source);
1746        assert_eq!(eval, EgressEvaluation::Deny);
1747    }
1748
1749    // ── extract_http_host ──────────────────────────────────────────────────────
1750
1751    #[test]
1752    fn extract_http_host_basic() {
1753        let buf = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1754        assert_eq!(extract_http_host(buf), Some("example.com".into()));
1755    }
1756
1757    #[test]
1758    fn extract_http_host_strips_port() {
1759        let buf = b"POST /api HTTP/1.1\r\nHost: api.company.com:8080\r\n\r\n";
1760        assert_eq!(extract_http_host(buf), Some("api.company.com".into()));
1761    }
1762
1763    #[test]
1764    fn extract_http_host_case_insensitive_lowercased() {
1765        let buf = b"GET / HTTP/1.1\r\nhost: Example.COM\r\n\r\n";
1766        assert_eq!(extract_http_host(buf), Some("example.com".into()));
1767    }
1768
1769    #[test]
1770    fn extract_http_host_no_host_header() {
1771        let buf = b"GET / HTTP/1.1\r\nX-Other: foo\r\n\r\n";
1772        assert_eq!(extract_http_host(buf), None);
1773    }
1774
1775    #[test]
1776    fn extract_http_host_incomplete_headers() {
1777        let buf = b"GET / HTTP/1.1\r\nHost: x";
1778        assert_eq!(extract_http_host(buf), None);
1779    }
1780
1781    #[test]
1782    fn extract_http_host_tls_first_byte() {
1783        let buf = [0x16u8, 0x03, 0x01, 0x00, 0x01];
1784        assert_eq!(extract_http_host(&buf), None);
1785    }
1786
1787    #[test]
1788    fn extract_http_host_with_many_headers() {
1789        // Far more headers than a small fixed parse array would hold: the Host
1790        // must still be found rather than the request looking hostless.
1791        let mut req = Vec::from(&b"GET / HTTP/1.1\r\n"[..]);
1792        for i in 0..100 {
1793            req.extend_from_slice(format!("X-Pad-{i}: v\r\n").as_bytes());
1794        }
1795        req.extend_from_slice(b"Host: example.com\r\n\r\n");
1796        assert_eq!(extract_http_host(&req), Some("example.com".into()));
1797    }
1798
1799    // ── plain-HTTP secret substitution ────────────────────────────────────────
1800
1801    use std::sync::Arc;
1802    use tokio::io::AsyncReadExt;
1803    use tokio::net::TcpListener;
1804    use tokio::task::JoinHandle;
1805
1806    use crate::secrets::config::{
1807        HostPattern, SecretEntry, SecretSubstitution, SecretViolationAction, SecretsConfig,
1808    };
1809
1810    fn make_plain_http_secret(placeholder: &str, value: &str, require_tls: bool) -> SecretsConfig {
1811        SecretsConfig {
1812            secrets: vec![SecretEntry {
1813                env_var: "API_KEY".into(),
1814                value: zeroize::Zeroizing::new(value.into()),
1815                source: None,
1816                placeholder: placeholder.into(),
1817                allowed_hosts: vec![HostPattern::Any],
1818                substitution: SecretSubstitution {
1819                    headers: true,
1820                    query: false,
1821                    body: false,
1822                },
1823                passthrough_hosts: Vec::new(),
1824                violation_action: None,
1825                require_tls_identity: require_tls,
1826            }],
1827            ..Default::default()
1828        }
1829    }
1830
1831    fn make_host_bound_secret(placeholder: &str, value: &str, host: &str) -> SecretsConfig {
1832        SecretsConfig {
1833            secrets: vec![SecretEntry {
1834                env_var: "API_KEY".into(),
1835                value: zeroize::Zeroizing::new(value.into()),
1836                source: None,
1837                placeholder: placeholder.into(),
1838                allowed_hosts: vec![HostPattern::Exact(host.into())],
1839                substitution: SecretSubstitution::default(),
1840                passthrough_hosts: Vec::new(),
1841                violation_action: None,
1842                require_tls_identity: true,
1843            }],
1844            ..Default::default()
1845        }
1846    }
1847
1848    #[test]
1849    fn sanitize_connect_headers_blocks_placeholder_metadata_header_by_default() {
1850        let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1851        let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\nUser-Agent: curl\r\n\r\n";
1852
1853        assert_eq!(
1854            sanitize_connect_headers(headers, &secrets),
1855            Err(SecretViolationAction::BlockAndLog)
1856        );
1857    }
1858
1859    #[test]
1860    fn sanitize_connect_headers_respects_block_and_terminate() {
1861        let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1862        secrets.violation_action = SecretViolationAction::BlockAndTerminate;
1863        let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\n\r\n";
1864
1865        assert_eq!(
1866            sanitize_connect_headers(headers, &secrets),
1867            Err(SecretViolationAction::BlockAndTerminate)
1868        );
1869    }
1870
1871    #[test]
1872    fn sanitize_connect_headers_respects_explicit_passthrough() {
1873        let mut secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1874        secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
1875        let headers = b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nProxy-Authorization: Bearer $MSB_KEY\r\n\r\n";
1876
1877        let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1878
1879        assert_eq!(sanitized.as_ref(), headers);
1880        assert!(
1881            !String::from_utf8_lossy(sanitized.as_ref()).contains("real-secret-value"),
1882            "passthrough must never substitute real secrets into CONNECT metadata"
1883        );
1884    }
1885
1886    #[test]
1887    fn sanitize_connect_headers_keeps_safe_metadata_headers() {
1888        let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1889        let headers =
1890            b"CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\nUser-Agent: curl\r\n\r\n";
1891
1892        let sanitized = sanitize_connect_headers(headers, &secrets).unwrap();
1893
1894        assert_eq!(sanitized.as_ref(), headers);
1895    }
1896
1897    #[test]
1898    fn sanitize_connect_headers_blocks_placeholder_in_request_line() {
1899        let secrets = make_host_bound_secret("$MSB_KEY", "real-secret-value", "example.com");
1900        let headers = b"CONNECT $MSB_KEY:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n";
1901
1902        assert_eq!(
1903            sanitize_connect_headers(headers, &secrets),
1904            Err(SecretViolationAction::BlockAndLog)
1905        );
1906    }
1907
1908    async fn spawn_sink() -> (SocketAddr, JoinHandle<Vec<u8>>) {
1909        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1910        let addr = listener.local_addr().unwrap();
1911        let handle = tokio::spawn(async move {
1912            let (mut stream, _) = listener.accept().await.unwrap();
1913            let mut received = Vec::new();
1914            let mut buf = vec![0u8; 4096];
1915            loop {
1916                match stream.read(&mut buf).await {
1917                    Ok(0) | Err(_) => break,
1918                    Ok(n) => received.extend_from_slice(&buf[..n]),
1919                }
1920            }
1921            received
1922        });
1923        (addr, handle)
1924    }
1925
1926    async fn assert_server_first_banner_is_immediate(
1927        policy: NetworkPolicy,
1928        tls_state: Option<Arc<TlsState>>,
1929    ) {
1930        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1931        let addr = listener.local_addr().unwrap();
1932        let server = tokio::spawn(async move {
1933            let (mut stream, _) = listener.accept().await.unwrap();
1934            stream.write_all(b"READY\n").await.unwrap();
1935            let mut received = Vec::new();
1936            stream.read_to_end(&mut received).await.unwrap();
1937        });
1938
1939        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
1940        let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
1941        spawn_tcp_proxy(
1942            &tokio::runtime::Handle::current(),
1943            addr,
1944            addr,
1945            from_rx,
1946            to_tx,
1947            Arc::new(SharedState::new(4)),
1948            Arc::new(policy),
1949            Arc::new(SecretsConfig::default()),
1950            tls_state,
1951            false,
1952            Arc::new(ProxyConnectState::new()),
1953            None,
1954        );
1955
1956        let banner = tokio::time::timeout(Duration::from_secs(1), to_rx.recv())
1957            .await
1958            .expect("server-first banner was delayed by a pre-connect peek")
1959            .expect("proxy closed before relaying the server-first banner");
1960        assert_eq!(banner, b"READY\n"[..]);
1961
1962        drop(from_tx);
1963        tokio::time::timeout(Duration::from_secs(7), server)
1964            .await
1965            .expect("proxy did not close the upstream connection")
1966            .unwrap();
1967    }
1968
1969    #[tokio::test]
1970    async fn server_first_connection_skips_unrelated_domain_policy_peek() {
1971        let policy = NetworkPolicy {
1972            default_egress: Action::Allow,
1973            default_ingress: Action::Allow,
1974            rules: vec![allow_https("unused.example")],
1975        };
1976
1977        assert_server_first_banner_is_immediate(policy, None).await;
1978    }
1979
1980    #[tokio::test]
1981    async fn server_first_connection_skips_eager_connect_peek() {
1982        let _ = rustls::crypto::ring::default_provider().install_default();
1983        let tls_state = Arc::new(
1984            TlsState::new(
1985                microsandbox_types::TlsConfig::default(),
1986                crate::secrets::handle::SecretsHandle::new(SecretsConfig::default()),
1987            )
1988            .unwrap(),
1989        );
1990
1991        assert_server_first_banner_is_immediate(NetworkPolicy::default(), Some(tls_state)).await;
1992    }
1993
1994    async fn relay_through_proxy(
1995        request: Vec<u8>,
1996        secrets: SecretsConfig,
1997        handle: JoinHandle<Vec<u8>>,
1998        server_addr: SocketAddr,
1999    ) -> Vec<u8> {
2000        relay_through_proxy_with_policy(
2001            request,
2002            Arc::new(SharedState::new(4)),
2003            Arc::new(NetworkPolicy::default()),
2004            secrets,
2005            handle,
2006            server_addr,
2007        )
2008        .await
2009    }
2010
2011    async fn relay_through_proxy_with_policy(
2012        request: Vec<u8>,
2013        shared: Arc<SharedState>,
2014        policy: Arc<NetworkPolicy>,
2015        secrets: SecretsConfig,
2016        handle: JoinHandle<Vec<u8>>,
2017        server_addr: SocketAddr,
2018    ) -> Vec<u8> {
2019        relay_chunks_through_proxy_with_policy(
2020            vec![request],
2021            shared,
2022            policy,
2023            secrets,
2024            handle,
2025            server_addr,
2026        )
2027        .await
2028    }
2029
2030    async fn relay_chunks_through_proxy_with_policy(
2031        chunks: Vec<Vec<u8>>,
2032        shared: Arc<SharedState>,
2033        policy: Arc<NetworkPolicy>,
2034        secrets: SecretsConfig,
2035        handle: JoinHandle<Vec<u8>>,
2036        server_addr: SocketAddr,
2037    ) -> Vec<u8> {
2038        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2039        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2040        let secrets = Arc::new(secrets);
2041        let proxy_connect = Arc::new(ProxyConnectState::new());
2042
2043        for chunk in chunks {
2044            from_tx.send(Bytes::from(chunk)).await.unwrap();
2045        }
2046        drop(from_tx);
2047
2048        TcpProxy::new(
2049            server_addr,
2050            UpstreamTcpTarget::direct(server_addr),
2051            from_rx,
2052            to_tx,
2053            shared,
2054            policy,
2055            secrets,
2056            None,
2057            false,
2058            proxy_connect,
2059            None,
2060        )
2061        .try_run()
2062        .await
2063        .unwrap();
2064
2065        handle.await.unwrap()
2066    }
2067
2068    #[tokio::test]
2069    async fn plain_http_domain_policy_allows_matching_host() {
2070        let (addr, sink) = spawn_sink().await;
2071        let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2072        let policy = Arc::new(NetworkPolicy {
2073            default_egress: Action::Deny,
2074            default_ingress: Action::Allow,
2075            rules: vec![allow_tcp("allowed.example", addr.port())],
2076        });
2077
2078        let wire = relay_through_proxy_with_policy(
2079            b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2080            shared,
2081            policy,
2082            SecretsConfig::default(),
2083            sink,
2084            addr,
2085        )
2086        .await;
2087
2088        assert_eq!(wire, b"GET / HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2089    }
2090
2091    #[tokio::test]
2092    async fn plain_http_domain_policy_blocks_host_switch() {
2093        let (addr, sink) = spawn_sink().await;
2094        let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2095        let policy = Arc::new(NetworkPolicy {
2096            default_egress: Action::Deny,
2097            default_ingress: Action::Allow,
2098            rules: vec![allow_tcp("allowed.example", addr.port())],
2099        });
2100
2101        let wire = relay_through_proxy_with_policy(
2102            b"GET / HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2103            shared,
2104            policy,
2105            SecretsConfig::default(),
2106            sink,
2107            addr,
2108        )
2109        .await;
2110
2111        assert!(
2112            wire.is_empty(),
2113            "switched HTTP authority must not reach upstream, got: {wire:?}"
2114        );
2115    }
2116
2117    #[tokio::test]
2118    async fn plain_http_domain_policy_blocks_keep_alive_host_switch() {
2119        let (addr, sink) = spawn_sink().await;
2120        let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2121        let policy = Arc::new(NetworkPolicy {
2122            default_egress: Action::Deny,
2123            default_ingress: Action::Allow,
2124            rules: vec![allow_tcp("allowed.example", addr.port())],
2125        });
2126
2127        let wire = relay_chunks_through_proxy_with_policy(
2128            vec![
2129                b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n".to_vec(),
2130                b"GET /two HTTP/1.1\r\nHost: denied.example\r\n\r\n".to_vec(),
2131            ],
2132            shared,
2133            policy,
2134            SecretsConfig::default(),
2135            sink,
2136            addr,
2137        )
2138        .await;
2139
2140        assert_eq!(wire, b"GET /one HTTP/1.1\r\nHost: allowed.example\r\n\r\n");
2141    }
2142
2143    #[test]
2144    fn strict_hostname_allow_blocks_sni_authority_before_tcp_dial() {
2145        let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2146        let shared = shared_with("allowed.example", "127.0.0.1");
2147        let policy = NetworkPolicy {
2148            default_egress: Action::Deny,
2149            default_ingress: Action::Allow,
2150            rules: vec![allow_tcp("allowed.example", dst.port())],
2151        };
2152
2153        assert!(strict_hostname_allow_is_opaque(
2154            true,
2155            &policy,
2156            dst,
2157            &shared,
2158            Some("allowed.example"),
2159            &synthetic_client_hello("allowed.example"),
2160        ));
2161    }
2162
2163    #[test]
2164    fn strict_hostname_allow_blocks_tls_without_sni_before_tcp_dial() {
2165        let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2166        let shared = shared_with("allowed.example", "127.0.0.1");
2167        let policy = NetworkPolicy {
2168            default_egress: Action::Deny,
2169            default_ingress: Action::Allow,
2170            rules: vec![allow_tcp("allowed.example", dst.port())],
2171        };
2172
2173        assert!(strict_hostname_allow_is_opaque(
2174            true,
2175            &policy,
2176            dst,
2177            &shared,
2178            None,
2179            &[0x16, 0x03, 0x01],
2180        ));
2181    }
2182
2183    #[test]
2184    fn strict_hostname_allow_leaves_plain_http_for_authority_validation() {
2185        let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 80);
2186        let shared = shared_with("allowed.example", "127.0.0.1");
2187        let policy = NetworkPolicy {
2188            default_egress: Action::Deny,
2189            default_ingress: Action::Allow,
2190            rules: vec![allow_tcp("allowed.example", dst.port())],
2191        };
2192
2193        assert!(!strict_hostname_allow_is_opaque(
2194            true,
2195            &policy,
2196            dst,
2197            &shared,
2198            None,
2199            b"GET / HTTP/1.1\r\n",
2200        ));
2201    }
2202
2203    #[tokio::test]
2204    async fn strict_mode_blocks_hostname_allowed_opaque_tls() {
2205        let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 443);
2206        let shared = Arc::new(shared_with("allowed.example", "127.0.0.1"));
2207        let policy = Arc::new(NetworkPolicy {
2208            default_egress: Action::Deny,
2209            default_ingress: Action::Allow,
2210            rules: vec![allow_tcp("allowed.example", dst.port())],
2211        });
2212        let proxy_connect = Arc::new(ProxyConnectState::new());
2213        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2214        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2215
2216        from_tx
2217            .send(Bytes::from(synthetic_client_hello("allowed.example")))
2218            .await
2219            .unwrap();
2220        drop(from_tx);
2221
2222        TcpProxy::new(
2223            dst,
2224            UpstreamTcpTarget::direct(dst),
2225            from_rx,
2226            to_tx,
2227            shared,
2228            policy,
2229            Arc::new(SecretsConfig::default()),
2230            None,
2231            true,
2232            proxy_connect.clone(),
2233            None,
2234        )
2235        .try_run()
2236        .await
2237        .unwrap();
2238
2239        assert_eq!(proxy_connect.status(), ProxyConnectStatus::PolicyDenied);
2240    }
2241
2242    #[tokio::test]
2243    async fn strict_mode_leaves_default_allowed_opaque_tls_to_policy() {
2244        let dst = SocketAddr::new("127.0.0.1".parse().unwrap(), 9);
2245        let shared = Arc::new(SharedState::new(4));
2246        let policy = Arc::new(NetworkPolicy {
2247            default_egress: Action::Allow,
2248            default_ingress: Action::Allow,
2249            rules: vec![Rule::deny_egress(Destination::Domain(
2250                "blocked.example".parse().unwrap(),
2251            ))],
2252        });
2253        let proxy_connect = Arc::new(ProxyConnectState::new());
2254        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2255        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2256
2257        from_tx
2258            .send(Bytes::from(synthetic_client_hello("allowed.example")))
2259            .await
2260            .unwrap();
2261        drop(from_tx);
2262
2263        let result = TcpProxy::new(
2264            dst,
2265            UpstreamTcpTarget::direct(dst),
2266            from_rx,
2267            to_tx,
2268            shared,
2269            policy,
2270            Arc::new(SecretsConfig::default()),
2271            None,
2272            true,
2273            proxy_connect.clone(),
2274            None,
2275        )
2276        .try_run()
2277        .await;
2278
2279        assert!(result.is_err(), "dummy upstream should refuse the dial");
2280        assert_eq!(
2281            proxy_connect.status(),
2282            ProxyConnectStatus::UpstreamConnectFailed
2283        );
2284    }
2285
2286    #[tokio::test]
2287    async fn server_first_http_like_binary_first_flight_is_forwarded() {
2288        use tokio::net::TcpListener;
2289
2290        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2291        let addr = listener.local_addr().unwrap();
2292        let server = tokio::spawn(async move {
2293            let (mut stream, _) = listener.accept().await.unwrap();
2294            stream
2295                .write_all(b"binary server-first greeting")
2296                .await
2297                .unwrap();
2298            stream.flush().await.unwrap();
2299
2300            let mut received = Vec::new();
2301            stream.read_to_end(&mut received).await.unwrap();
2302            received
2303        });
2304
2305        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2306        let (to_tx, mut to_rx) = mpsc::channel::<Bytes>(8);
2307        spawn_tcp_proxy(
2308            &tokio::runtime::Handle::current(),
2309            addr,
2310            addr,
2311            from_rx,
2312            to_tx,
2313            Arc::new(SharedState::new(4)),
2314            Arc::new(NetworkPolicy::default()),
2315            Arc::new(make_plain_http_secret(
2316                "$MSB_UNUSED",
2317                "unused-secret-value",
2318                false,
2319            )),
2320            None,
2321            false,
2322            Arc::new(ProxyConnectState::new()),
2323            None,
2324        );
2325
2326        let greeting = to_rx.recv().await.unwrap();
2327        assert_eq!(greeting, b"binary server-first greeting"[..]);
2328
2329        // `BINARY3` is deliberately a valid HTTP token followed by a space,
2330        // but the control bytes make this an invalid HTTP request line.
2331        let first_flight = Bytes::from_static(b"BINARY3 v1\x00\x01opaque request");
2332        from_tx.send(first_flight.clone()).await.unwrap();
2333        drop(from_tx);
2334
2335        let wire = tokio::time::timeout(Duration::from_secs(2), server)
2336            .await
2337            .expect("proxy did not finish forwarding the client first flight")
2338            .unwrap();
2339        assert_eq!(wire, first_flight);
2340    }
2341
2342    #[tokio::test]
2343    async fn plain_http_substitutes_placeholder_when_host_arrives_in_second_segment() {
2344        // Host header split across TCP segments — classify_first_flight must keep
2345        // reading until \r\n\r\n before extract_http_host is called.
2346        let (addr, sink) = spawn_sink().await;
2347        let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2348
2349        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2350        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2351        let proxy_connect = Arc::new(ProxyConnectState::new());
2352
2353        from_tx
2354            .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2355            .await
2356            .unwrap();
2357        from_tx
2358            .send(Bytes::from_static(
2359                b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2360            ))
2361            .await
2362            .unwrap();
2363        drop(from_tx);
2364
2365        TcpProxy::new(
2366            addr,
2367            UpstreamTcpTarget::direct(addr),
2368            from_rx,
2369            to_tx,
2370            Arc::new(SharedState::new(4)),
2371            Arc::new(NetworkPolicy::default()),
2372            Arc::new(secrets),
2373            None,
2374            false,
2375            proxy_connect,
2376            None,
2377        )
2378        .try_run()
2379        .await
2380        .unwrap();
2381
2382        let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2383        assert!(wire.contains("real-secret-value"), "got: {wire:?}");
2384        assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2385    }
2386
2387    #[tokio::test]
2388    async fn plain_http_passthrough_handles_a_host_in_split_headers() {
2389        // A default (require_tls_identity = true) host-bound secret is never substituted over plain
2390        // HTTP. Explicit passthrough allows its placeholder to remain unchanged even when the Host
2391        // arrives in a later segment than the request line.
2392        let (addr, sink) = spawn_sink().await;
2393
2394        let shared = SharedState::new(4);
2395        shared.cache_resolved_hostname(
2396            "example.com",
2397            ResolvedHostnameFamily::Ipv4,
2398            ["127.0.0.1".parse::<IpAddr>().unwrap()],
2399            StdDuration::from_secs(60),
2400        );
2401
2402        let secrets = SecretsConfig {
2403            secrets: vec![SecretEntry {
2404                env_var: "API_KEY".into(),
2405                value: zeroize::Zeroizing::new("real-secret-value".into()),
2406                source: None,
2407                placeholder: "$MSB_KEY".into(),
2408                allowed_hosts: vec![HostPattern::Exact("example.com".into())],
2409                substitution: SecretSubstitution {
2410                    headers: true,
2411                    query: false,
2412                    body: false,
2413                },
2414                passthrough_hosts: vec![HostPattern::Exact("example.com".into())],
2415                violation_action: None,
2416                require_tls_identity: true,
2417            }],
2418            ..Default::default()
2419        };
2420
2421        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2422        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2423        let proxy_connect = Arc::new(ProxyConnectState::new());
2424
2425        from_tx
2426            .send(Bytes::from_static(b"GET /api HTTP/1.1\r\n"))
2427            .await
2428            .unwrap();
2429        from_tx
2430            .send(Bytes::from_static(
2431                b"Host: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n",
2432            ))
2433            .await
2434            .unwrap();
2435        drop(from_tx);
2436
2437        TcpProxy::new(
2438            addr,
2439            UpstreamTcpTarget::direct(addr),
2440            from_rx,
2441            to_tx,
2442            Arc::new(shared),
2443            Arc::new(NetworkPolicy::default()),
2444            Arc::new(secrets),
2445            None,
2446            false,
2447            proxy_connect,
2448            None,
2449        )
2450        .try_run()
2451        .await
2452        .unwrap();
2453
2454        let wire = String::from_utf8(sink.await.unwrap()).unwrap();
2455        assert!(
2456            wire.contains("Host: example.com"),
2457            "request must reach the allowed host, got: {wire:?}"
2458        );
2459        assert!(
2460            wire.contains("$MSB_KEY"),
2461            "placeholder must be forwarded unchanged for a require_tls_identity secret, got: {wire:?}"
2462        );
2463        assert!(
2464            !wire.contains("real-secret-value"),
2465            "secret must never be substituted over plain HTTP, got: {wire:?}"
2466        );
2467    }
2468
2469    #[tokio::test]
2470    async fn plain_http_substitutes_placeholder_in_first_flight() {
2471        let (addr, sink) = spawn_sink().await;
2472
2473        let request =
2474            b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2475                .to_vec();
2476        let secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", false);
2477
2478        let wire =
2479            String::from_utf8(relay_through_proxy(request, secrets, sink, addr).await).unwrap();
2480        assert!(
2481            wire.contains("real-secret-value"),
2482            "real value must reach server, got: {wire:?}"
2483        );
2484        assert!(
2485            !wire.contains("$MSB_KEY"),
2486            "placeholder must not reach server, got: {wire:?}"
2487        );
2488    }
2489
2490    #[tokio::test]
2491    async fn plain_http_no_substitution_when_require_tls_identity_true() {
2492        let (addr, sink) = spawn_sink().await;
2493
2494        let request =
2495            b"GET /api HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\n\r\n"
2496                .to_vec();
2497        let mut secrets = make_plain_http_secret("$MSB_KEY", "real-secret-value", true);
2498        secrets.secrets[0].passthrough_hosts = vec![HostPattern::Any];
2499
2500        let wire =
2501            String::from_utf8_lossy(&relay_through_proxy(request, secrets, sink, addr).await)
2502                .into_owned();
2503        assert!(
2504            wire.contains("$MSB_KEY"),
2505            "placeholder must be forwarded unchanged when require_tls_identity=true, got: {wire:?}"
2506        );
2507        assert!(
2508            !wire.contains("real-secret-value"),
2509            "real value must not leak when require_tls_identity=true, got: {wire:?}"
2510        );
2511    }
2512
2513    #[tokio::test]
2514    async fn plain_http_large_body_forwarded_verbatim_in_relay_loop() {
2515        // Body arrives in a separate segment after headers — flows through the relay
2516        // loop, not the peek path. Ensures no bytes are dropped and header substitution
2517        // still happens.
2518        let (addr, sink) = spawn_sink().await;
2519        let secrets = make_plain_http_secret("$MSB_KEY", "real-value", false);
2520
2521        let body = "x".repeat(32_000);
2522        let header = format!(
2523            "POST /upload HTTP/1.1\r\nHost: example.com\r\nAuthorization: Bearer $MSB_KEY\r\nContent-Length: {}\r\n\r\n",
2524            body.len()
2525        );
2526
2527        let (from_tx, from_rx) = mpsc::channel::<Bytes>(8);
2528        let (to_tx, _to_rx) = mpsc::channel::<Bytes>(8);
2529        let proxy_connect = Arc::new(ProxyConnectState::new());
2530
2531        from_tx
2532            .send(Bytes::from(header.into_bytes()))
2533            .await
2534            .unwrap();
2535        from_tx
2536            .send(Bytes::from(body.clone().into_bytes()))
2537            .await
2538            .unwrap();
2539        drop(from_tx);
2540
2541        TcpProxy::new(
2542            addr,
2543            UpstreamTcpTarget::direct(addr),
2544            from_rx,
2545            to_tx,
2546            Arc::new(SharedState::new(4)),
2547            Arc::new(NetworkPolicy::default()),
2548            Arc::new(secrets),
2549            None,
2550            false,
2551            proxy_connect,
2552            None,
2553        )
2554        .try_run()
2555        .await
2556        .unwrap();
2557
2558        let wire = String::from_utf8_lossy(&sink.await.unwrap()).into_owned();
2559        assert!(wire.contains(&body), "got {} bytes", wire.len());
2560        assert!(!wire.contains("$MSB_KEY"), "got: {wire:?}");
2561    }
2562}