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