Skip to main content

ts_runtime/
serve.rs

1//! Stored Serve config + accept-loop runtime (`tsnet`'s `Get/SetServeConfig` + serving runtime).
2//!
3//! Go `tsnet` stores an `ipn.ServeConfig` on the node and runs one accept loop per configured
4//! tailnet port, dispatching each accepted connection per its handler (proxy / text / raw TCP
5//! forward / hand-back). This module is the faithful equivalent on the **application** netstack: a
6//! [`ServeManager`](crate::serve::ServeManager) owns the current [`ServeState`](ts_control::ServeState), one accept-loop task
7//! per bound port, and tears every loop down on drop / on the next `set`.
8//!
9//! ## Storage + reconcile (full-replace)
10//!
11//! The manager holds the current [`ServeState`](ts_control::ServeState) plus one [`tokio::task::AbortHandle`] per bound
12//! port behind a single `Arc<Mutex<Inner>>` (mirroring [`crate::fallback_tcp::FallbackTcpManager`]).
13//! [`ServeManager::set`](crate::serve::ServeManager::set) uses **full-replace** semantics: it aborts *every* existing accept loop and
14//! respawns from the new config. Go reconciles incrementally (leaving unchanged ports running); we
15//! do full-replace because it is simpler and correct, and a `SetServeConfig` is a rare control-plane
16//! operation, not a hot path. The passed [`ServeState`](ts_control::ServeState) becomes the whole config (REPLACE, matching
17//! Go). `pure_reconcile` computes the add/remove port deltas for testing and documentation, even
18//! though the live path replaces wholesale.
19//!
20//! ## TLS termination
21//!
22//! TLS-terminating ports (`ServeTarget::terminates_tls`) need a `TlsAcceptor`; the caller
23//! (`Device::set_serve_config`) obtains it **once** via the cert path and hands it in per port. The
24//! manager never builds an acceptor and never touches the cert/ACME machinery — that keeps
25//! `ts_runtime` off the cert path and lets the device fail the whole `set` closed if a cert cannot
26//! be issued (no plaintext downgrade).
27//!
28//! ## Anti-leak
29//!
30//! Every accept loop binds the **overlay** netstack only (via `Channel::tcp_listen` on the
31//! device's own tailnet IPv4) — never a host socket. The `ServeTarget::Proxy` /
32//! `ServeTarget::TcpForward` backend dial is a **local host socket** to the embedder's own backend
33//! (exactly like Go's reverse-proxy to `127.0.0.1` and like [`crate::Runtime`]'s loopback proxy) —
34//! it is intentionally NOT routed through the `ts_forwarder` exit-egress path, so the exit-node
35//! anti-leak chokepoint is untouched. A backend dial failure drops the connection (fail-closed,
36//! logged); it never falls back to anything.
37
38use std::{
39    collections::{BTreeMap, BTreeSet},
40    net::{Ipv4Addr, SocketAddr},
41    sync::{Arc, Mutex},
42};
43
44use netstack::{CreateSocket, netcore::Channel, netsock::TcpStream as OverlayStream};
45use tokio::{
46    io::{AsyncRead, AsyncWrite, AsyncWriteExt},
47    sync::{Semaphore, mpsc},
48};
49use ts_control::{ServeState, ServeTarget, tls::TlsAcceptor};
50
51/// Max concurrent in-flight connections served per bound port. Bounds the per-port spawn fan-out so
52/// a flood of accepts on one serve port cannot grow tasks (and overlay sockets) without limit;
53/// saturated => the accept loop back-pressures (stops accepting) until an in-flight conn finishes.
54/// Mirrors the loopback proxy's `MAX_CONCURRENT_CONNS` rationale (each accepted conn pins an overlay
55/// TCP socket, ~512 KiB of rx+tx buffers — see `tcp_buffer_size` in AGENTS.md).
56const MAX_SERVE_CONNS_PER_PORT: usize = 256;
57
58/// A connection handed back to the embedder for a [`ServeTarget::Accept`] port (the in-process
59/// stand-in for Go `tsnet`'s `ListenTLS`-returned `net.Listener`).
60///
61/// `stream` is already TLS-terminated (the overlay stream wrapped in `tokio_rustls`'s server
62/// `TlsStream`), boxed so the channel is target-agnostic. `port` is the serve port it arrived on so
63/// an embedder serving `Accept` on several ports can demultiplex.
64pub struct ServeAccepted {
65    /// The tailnet (overlay) port this connection was accepted on.
66    pub port: u16,
67    /// The accepted, TLS-terminated stream, ready to read/write.
68    pub stream: Box<dyn AsyncReadWrite>,
69}
70
71/// Object-safe alias for the boxed accepted stream: an `AsyncRead + AsyncWrite` the embedder drives.
72pub trait AsyncReadWrite: AsyncRead + AsyncWrite + Send + Unpin {}
73impl<T: AsyncRead + AsyncWrite + Send + Unpin> AsyncReadWrite for T {}
74
75/// Receiver side of the [`ServeTarget::Accept`] hand-back channel (mirrors a `net.Listener`'s accept
76/// queue). [`ServeManager::set`] returns one; await [`recv`](mpsc::Receiver::recv) to take the next
77/// accepted, TLS-terminated connection. Dropped/replaced when the next `set` runs.
78pub type ServeAcceptedReceiver = mpsc::Receiver<ServeAccepted>;
79
80/// A fully-resolved per-port serve plan: the target plus, for TLS-terminating targets, the acceptor
81/// the device built up-front from the cert path. The caller guarantees `acceptor.is_some()` exactly
82/// when `target.terminates_tls()` — the manager asserts this is never violated by failing the bind.
83pub struct ResolvedPort {
84    /// What to serve on this port.
85    pub target: ServeTarget,
86    /// The TLS acceptor for this port, present iff `target.terminates_tls()`.
87    pub acceptor: Option<TlsAcceptor>,
88}
89
90/// Shared manager state behind a single lock.
91struct Inner {
92    /// The currently-stored config (what [`get`](ServeManager::get) returns). Empty default until
93    /// the first `set`.
94    state: ServeState,
95    /// One accept-loop abort handle per currently-bound port. Aborting a handle stops that port's
96    /// accept loop (and, transitively, drops its listener so the overlay port is released).
97    ports: BTreeMap<u16, tokio::task::AbortHandle>,
98}
99
100impl Drop for Inner {
101    fn drop(&mut self) {
102        for h in self.ports.values() {
103            h.abort();
104        }
105    }
106}
107
108/// Owns the stored Serve config and the live per-port accept loops (`tsnet` serving runtime).
109///
110/// Built once from the application netstack [`Channel`] and the device's overlay IPv4, held by the
111/// [`crate::Runtime`]. [`set`](Self::set) replaces the whole config (full-replace reconcile);
112/// dropping the manager (with the runtime / device) aborts every accept loop.
113pub struct ServeManager {
114    inner: Arc<Mutex<Inner>>,
115    channel: Channel,
116    self_ipv4: Ipv4Addr,
117}
118
119impl ServeManager {
120    /// Build a manager bound to the application netstack `channel` and the device's own tailnet
121    /// `self_ipv4` (the overlay address every serve listener binds on). No accept loop runs until the
122    /// first [`set`](Self::set).
123    pub fn new(channel: Channel, self_ipv4: Ipv4Addr) -> Self {
124        Self {
125            inner: Arc::new(Mutex::new(Inner {
126                state: ServeState::default(),
127                ports: BTreeMap::new(),
128            })),
129            channel,
130            self_ipv4,
131        }
132    }
133
134    /// The currently-stored config (Go `GetServeConfig`); empty default if none was ever set.
135    pub fn get(&self) -> ServeState {
136        self.inner
137            .lock()
138            .unwrap_or_else(|e| e.into_inner())
139            .state
140            .clone()
141    }
142
143    /// Replace the whole Serve config (Go `SetServeConfig`, REPLACE semantics), full-replace
144    /// reconcile.
145    ///
146    /// `state` is the new config; `resolved` carries the per-port target + (for TLS ports) the
147    /// pre-built acceptor, keyed identically to `state.ports`. Aborts every existing accept loop and
148    /// spawns one per port in `resolved`. Returns a fresh [`ServeAcceptedReceiver`] delivering
149    /// connections for every [`ServeTarget::Accept`] port (empty if there are none).
150    ///
151    /// The caller is responsible for `state.validate()` and for obtaining the acceptors (failing the
152    /// whole call closed if a cert can't be issued) before calling this; the manager only binds and
153    /// dispatches.
154    pub fn set(
155        &self,
156        state: ServeState,
157        resolved: BTreeMap<u16, ResolvedPort>,
158    ) -> ServeAcceptedReceiver {
159        // A bounded channel back-pressures a slow embedder rather than buffering unboundedly.
160        let (accept_tx, accept_rx) = mpsc::channel::<ServeAccepted>(MAX_SERVE_CONNS_PER_PORT);
161
162        let mut new_ports: BTreeMap<u16, tokio::task::AbortHandle> = BTreeMap::new();
163        for (port, rp) in resolved {
164            let channel = self.channel.clone();
165            let self_ipv4 = self.self_ipv4;
166            let accept_tx = accept_tx.clone();
167            let handle = tokio::spawn(async move {
168                if let Err(e) = run_port(channel, self_ipv4, port, rp, accept_tx).await {
169                    tracing::warn!(%port, error = %e, "serve listener exited");
170                }
171            })
172            .abort_handle();
173            new_ports.insert(port, handle);
174        }
175
176        // Swap in the new state + handles under the lock; aborting the OLD handles happens when the
177        // replaced map is dropped at end of scope (after the lock is released).
178        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
179        inner.state = state;
180        let old = std::mem::replace(&mut inner.ports, new_ports);
181        drop(inner);
182        for h in old.values() {
183            h.abort();
184        }
185
186        accept_rx
187    }
188}
189
190/// Compute which ports must be added and removed to go from `current` to `next` (pure; the diff Go
191/// reconciles incrementally). The live [`ServeManager::set`] uses full-replace, but this captures
192/// the delta for tests/documentation: a port is *changed* iff its target differs, which counts as
193/// both a remove and an add.
194#[cfg_attr(not(test), allow(dead_code))]
195fn pure_reconcile(
196    current: &BTreeMap<u16, ServeTarget>,
197    next: &BTreeMap<u16, ServeTarget>,
198) -> (BTreeSet<u16>, BTreeSet<u16>) {
199    let mut to_add = BTreeSet::new();
200    let mut to_remove = BTreeSet::new();
201    for (port, target) in next {
202        match current.get(port) {
203            Some(cur) if cur == target => {}
204            _ => {
205                to_add.insert(*port);
206            }
207        }
208    }
209    for port in current.keys() {
210        match next.get(port) {
211            Some(target) if current.get(port) == Some(target) => {}
212            _ => {
213                to_remove.insert(*port);
214            }
215        }
216    }
217    (to_add, to_remove)
218}
219
220/// Accept loop for one serve port: bind the overlay listener on `(self_ipv4, port)` and dispatch
221/// each accepted connection per `rp.target`, capped at [`MAX_SERVE_CONNS_PER_PORT`] in flight.
222async fn run_port(
223    channel: Channel,
224    self_ipv4: Ipv4Addr,
225    port: u16,
226    rp: ResolvedPort,
227    accept_tx: mpsc::Sender<ServeAccepted>,
228) -> Result<(), netstack::netcore::Error> {
229    // Anti-leak: bind the OVERLAY netstack on this node's own tailnet IPv4, never a host socket.
230    let listen_addr = SocketAddr::new(self_ipv4.into(), port);
231    let listener = channel.tcp_listen(listen_addr).await?;
232    tracing::debug!(%port, "serve listener accepting");
233
234    let rp = Arc::new(rp);
235    let inflight = Arc::new(Semaphore::new(MAX_SERVE_CONNS_PER_PORT));
236
237    loop {
238        // Acquire a permit BEFORE accepting so the loop back-pressures at the cap.
239        let Ok(permit) = inflight.clone().acquire_owned().await else {
240            return Ok(());
241        };
242        let overlay = listener.accept().await?;
243
244        let rp = rp.clone();
245        let accept_tx = accept_tx.clone();
246        tokio::spawn(async move {
247            let _permit = permit; // released when this connection finishes
248            dispatch_conn(port, overlay, rp, accept_tx).await;
249        });
250    }
251}
252
253/// Dispatch one accepted overlay connection per the port's target. TLS is terminated here (once per
254/// connection) for TLS-terminating targets; failures drop the connection (fail-closed, logged).
255async fn dispatch_conn(
256    port: u16,
257    overlay: OverlayStream,
258    rp: Arc<ResolvedPort>,
259    accept_tx: mpsc::Sender<ServeAccepted>,
260) {
261    match &rp.target {
262        // Raw passthrough: NO TLS. Splice the raw overlay stream to the local backend.
263        ServeTarget::TcpForward { to } => {
264            forward_to_backend(port, overlay, to).await;
265        }
266        // TLS-terminating targets: terminate TLS once, then act on the decrypted stream.
267        _ => {
268            let Some(acceptor) = rp.acceptor.as_ref() else {
269                // The caller's contract guarantees a TLS acceptor for every TLS-terminating port;
270                // a missing one means we must never serve plaintext — drop, fail-closed.
271                tracing::warn!(%port, "serve: missing TLS acceptor for TLS port; dropping conn");
272                return;
273            };
274            let tls = match acceptor.accept(overlay).await {
275                Ok(s) => s,
276                Err(e) => {
277                    tracing::debug!(%port, error = %e, "serve: TLS handshake failed; dropping conn");
278                    return;
279                }
280            };
281            match &rp.target {
282                ServeTarget::Accept => {
283                    // Hand the TLS-terminated stream back to the embedder over the channel.
284                    let accepted = ServeAccepted {
285                        port,
286                        stream: Box::new(tls),
287                    };
288                    if accept_tx.send(accepted).await.is_err() {
289                        tracing::debug!(%port, "serve: accept receiver dropped; closing conn");
290                    }
291                }
292                // Reached DIRECTLY (no request head consumed off `tls`): a plain splice with no
293                // prefix replay — the backend sees the client's bytes verbatim.
294                ServeTarget::Proxy { to } => {
295                    proxy_to_backend(port, tls, to).await;
296                }
297                ServeTarget::Text { body } => {
298                    write_text(port, tls, body).await;
299                }
300                ServeTarget::Redirect { to, status } => {
301                    serve_redirect(port, tls, to, *status).await;
302                }
303                ServeTarget::Path { handlers } => {
304                    serve_path(port, tls, handlers).await;
305                }
306                // `TcpForward` is handled in the non-TLS arm above; nothing else terminates TLS.
307                // The wildcard covers `#[non_exhaustive]` future raw (non-TLS) variants: if one is
308                // added it must NOT silently terminate TLS here — drop it fail-closed until this
309                // dispatch is taught how to serve it.
310                other => {
311                    debug_assert!(
312                        !other.terminates_tls(),
313                        "TLS-terminating ServeTarget reached fall-through arm"
314                    );
315                    tracing::warn!(%port, "serve: unhandled ServeTarget on TLS port; dropping conn");
316                }
317            }
318        }
319    }
320}
321
322/// Reverse-proxy a TLS-terminated stream to a local host backend (Go `Proxy` handler). The backend
323/// dial is a LOCAL host socket to the embedder's own backend — never the forwarder egress path.
324///
325/// Reached DIRECTLY from [`dispatch_conn`] (no request head has been consumed off `tls`), so no
326/// prefix replay is needed — the backend sees the client's bytes verbatim via the bidirectional
327/// splice. The `Path`-nested case (where a head WAS consumed) uses [`proxy_to_backend_with_prefix`]
328/// instead.
329async fn proxy_to_backend<S>(port: u16, tls: S, to: &str)
330where
331    S: AsyncRead + AsyncWrite + Unpin,
332{
333    proxy_to_backend_with_prefix(port, tls, to, &[]).await;
334}
335
336/// Reverse-proxy a TLS-terminated stream to a local host backend, writing `prefix` to the backend
337/// FIRST (before the bidirectional splice). This replays an HTTP request head already consumed off
338/// `tls` (e.g. by [`serve_path`]'s [`read_http_head`]) so the backend sees the complete request: the
339/// consumed request line + headers, then the rest of the body/stream via the splice. An empty
340/// `prefix` is equivalent to a plain splice ([`proxy_to_backend`]). The backend dial is a LOCAL host
341/// socket — never the forwarder egress path; any failure (dial or prefix write) drops the conn
342/// fail-closed.
343async fn proxy_to_backend_with_prefix<S>(port: u16, mut tls: S, to: &str, prefix: &[u8])
344where
345    S: AsyncRead + AsyncWrite + Unpin,
346{
347    let mut backend = match tokio::net::TcpStream::connect(to).await {
348        Ok(b) => b,
349        Err(e) => {
350            tracing::debug!(%port, %to, error = %e, "serve proxy: backend dial failed; dropping conn");
351            return;
352        }
353    };
354    if !prefix.is_empty()
355        && let Err(e) = backend.write_all(prefix).await
356    {
357        tracing::debug!(%port, %to, error = %e, "serve proxy: prefix replay failed; dropping conn");
358        return;
359    }
360    if let Err(e) = tokio::io::copy_bidirectional(&mut tls, &mut backend).await {
361        tracing::debug!(%port, %to, error = %e, "serve proxy: splice ended");
362    }
363}
364
365/// Forward a RAW (non-TLS) overlay stream to a local host backend (Go `TCPForward` handler). The
366/// backend dial is a LOCAL host socket — never the forwarder egress path.
367async fn forward_to_backend(port: u16, mut overlay: OverlayStream, to: &str) {
368    let mut backend = match tokio::net::TcpStream::connect(to).await {
369        Ok(b) => b,
370        Err(e) => {
371            tracing::debug!(%port, %to, error = %e, "serve forward: backend dial failed; dropping conn");
372            return;
373        }
374    };
375    if let Err(e) = tokio::io::copy_bidirectional(&mut overlay, &mut backend).await {
376        tracing::debug!(%port, %to, error = %e, "serve forward: splice ended");
377    }
378}
379
380/// Write a fixed body to the TLS-terminated stream, flush, and close (Go `Text` handler).
381async fn write_text<S>(port: u16, mut tls: S, body: &str)
382where
383    S: AsyncRead + AsyncWrite + Unpin,
384{
385    if let Err(e) = tls.write_all(body.as_bytes()).await {
386        tracing::debug!(%port, error = %e, "serve text: write failed");
387        return;
388    }
389    if let Err(e) = tls.flush().await {
390        tracing::debug!(%port, error = %e, "serve text: flush failed");
391    }
392    drop(tls.shutdown().await);
393}
394
395/// Max bytes of an HTTP request head (request line + headers) we will buffer before giving up. A
396/// peer that never sends `\r\n\r\n` within this exact bound is dropped fail-closed (no unbounded
397/// read); the buffer is bound-checked AFTER each read, so it never exceeds this cap.
398const MAX_HTTP_HEAD: usize = 8 * 1024;
399
400/// Read the HTTP request head (up to and including `\r\n\r\n`) from a TLS-terminated stream into a
401/// buffer. Returns `(buf, header_end)` where `header_end` is the offset just past the terminator, or
402/// `None` if the peer closed early or the head exceeded [`MAX_HTTP_HEAD`]. Hand-rolled (no
403/// axum/hyper); mirrors the peerAPI router's head-read style.
404async fn read_http_head<S>(stream: &mut S) -> Option<(Vec<u8>, usize)>
405where
406    S: AsyncRead + AsyncWrite + Unpin,
407{
408    use tokio::io::AsyncReadExt;
409
410    let mut buf = Vec::with_capacity(1024);
411    let mut tmp = [0u8; 1024];
412    loop {
413        if let Some(end) = crate::peerapi_doh::find_header_end(&buf) {
414            return Some((buf, end));
415        }
416        match stream.read(&mut tmp).await {
417            Ok(0) => return None,
418            Ok(n) => {
419                buf.extend_from_slice(&tmp[..n]);
420                // Bound-check AFTER extending so the buffer never exceeds MAX_HTTP_HEAD. The
421                // terminator is re-checked at the top of the loop, so a head whose terminator lands
422                // exactly at the bound still succeeds; only a head with no terminator within
423                // MAX_HTTP_HEAD is dropped fail-closed.
424                if crate::peerapi_doh::find_header_end(&buf).is_none() && buf.len() >= MAX_HTTP_HEAD
425                {
426                    return None;
427                }
428            }
429            Err(_) => return None,
430        }
431    }
432}
433
434/// Parse the request-line path from an HTTP head. Returns the path component (without the query
435/// string), or `None` if the head is malformed. Hand-rolled; no HTTP library framing assumptions
436/// beyond the request line.
437fn request_path(buf: &[u8]) -> Option<String> {
438    let mut headers = [httparse::EMPTY_HEADER; 32];
439    let mut req = httparse::Request::new(&mut headers);
440    match req.parse(buf) {
441        Ok(_) => {}
442        Err(_) => return None,
443    }
444    let path = req.path?;
445    let raw = path.split_once('?').map(|(p, _)| p).unwrap_or(path);
446    Some(raw.to_string())
447}
448
449/// Reason phrase for a redirect status (best-effort; falls back to "Redirect").
450fn redirect_reason(status: u16) -> &'static str {
451    match status {
452        301 => "Moved Permanently",
453        302 => "Found",
454        303 => "See Other",
455        307 => "Temporary Redirect",
456        308 => "Permanent Redirect",
457        _ => "Redirect",
458    }
459}
460
461/// Write a bodyless HTTP redirect (Go `HTTPHandler` redirect) on a TLS-terminated stream, then close.
462/// Fail-closed: any write error drops the conn. No request parsing is needed — every request on a
463/// `Redirect` target gets the same response.
464async fn serve_redirect<S>(port: u16, mut tls: S, to: &str, status: u16)
465where
466    S: AsyncRead + AsyncWrite + Unpin,
467{
468    let head = format!(
469        "HTTP/1.1 {status} {reason}\r\nLocation: {to}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
470        reason = redirect_reason(status),
471    );
472    if let Err(e) = tls.write_all(head.as_bytes()).await {
473        tracing::debug!(%port, error = %e, "serve redirect: write failed");
474        return;
475    }
476    if let Err(e) = tls.flush().await {
477        tracing::debug!(%port, error = %e, "serve redirect: flush failed");
478    }
479    drop(tls.shutdown().await);
480}
481
482/// Write a bodyless HTTP status response (e.g. `404 Not Found`) on a TLS-terminated stream, then
483/// close. Local mirror of `peerapi_doh::write_status` (which takes the concrete peerAPI stream type).
484async fn write_http_status<S>(port: u16, mut tls: S, status: &str)
485where
486    S: AsyncRead + AsyncWrite + Unpin,
487{
488    let head = format!("HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
489    if let Err(e) = tls.write_all(head.as_bytes()).await {
490        tracing::debug!(%port, error = %e, "serve path: status write failed");
491        return;
492    }
493    drop(tls.flush().await);
494    drop(tls.shutdown().await);
495}
496
497/// Whether a mount point in a [`ServeTarget::Path`] map claims `path`.
498///
499/// A mount at `P` claims exactly `P` itself and the paths **below** it — i.e. `path == P`, or `path`
500/// begins with `P` followed by `/`. It does **not** claim arbitrary strings that merely start with
501/// the same bytes: a `/api` mount does not claim `/apifoo`, `/apibar` or `/api-internal`, which fall
502/// through to whatever shorter mount (typically `/`) does claim them.
503///
504/// A mount written with a trailing slash means the same thing as one without: `/api/` is normalized
505/// to `/api`, so it claims `/api/v2` without needing the request to be `/api//v2`, and it also
506/// claims the bare `/api`. The root mount `/` normalizes to the empty prefix and therefore claims
507/// every path.
508///
509/// ## Go behaviour this mirrors
510///
511/// Go's `getServeHandler` (`ipn/ipnlocal/serve.go`) never does a raw byte-prefix test. It first
512/// looks the cleaned request path up in the handler map exactly, and only then walks *backwards*
513/// over the path's `/` separators, retrying the lookup on each successively shorter truncation of
514/// the path. Because every candidate it ever tries is the path cut at a `/`, a handler can only ever
515/// be reached at a path-segment boundary — `/apifoo` never reaches the `/api` handler there, and it
516/// must not here either.
517fn mount_claims_path(mount: &str, path: &str) -> bool {
518    // "/api/" and "/api" are the same mount; "/" becomes the empty prefix, which claims everything.
519    let base = mount.strip_suffix('/').unwrap_or(mount);
520    if base.is_empty() {
521        return true;
522    }
523    match path.strip_prefix(base) {
524        // Exactly the mount itself, or a path below it. Anything else (`/apifoo` for `/api`) is a
525        // different path that merely shares a byte prefix.
526        Some(rest) => rest.is_empty() || rest.starts_with('/'),
527        None => false,
528    }
529}
530
531/// Pick the [`ServeTarget`] a request `path` dispatches to in a [`ServeTarget::Path`] mux.
532///
533/// Pure and total over `(handlers, path)` — the whole routing decision, with no I/O — so it is
534/// testable directly instead of only through a TLS-terminated socket. [`serve_path`] calls this; it
535/// is the single definition of the rule, and a test that re-implemented it would be testing its own
536/// copy rather than what dispatch does.
537///
538/// Longest match wins: of the mounts that claim `path` (see [`mount_claims_path`]), the one with the
539/// most path bytes is chosen, so `/api/v2` beats `/api` beats `/`. Ties (only reachable between the
540/// same mount spelled with and without a trailing slash, e.g. `/api` and `/api/`) resolve to the
541/// last in `BTreeMap` order, deterministically. `None` means no mount claims the path, which
542/// dispatch turns into a fail-closed 404.
543fn match_path_handler<'h>(
544    handlers: &'h BTreeMap<String, ServeTarget>,
545    path: &str,
546) -> Option<&'h ServeTarget> {
547    handlers
548        .iter()
549        .filter(|(mount, _)| mount_claims_path(mount, path))
550        .max_by_key(|(mount, _)| mount.strip_suffix('/').unwrap_or(mount).len())
551        .map(|(_, target)| target)
552}
553
554/// Serve a [`ServeTarget::Path`] mux on a TLS-terminated stream: read the request head, pick the
555/// longest-matching mount in `handlers` (via [`match_path_handler`]), and dispatch the matched
556/// nested target on the already-decrypted stream. Fail-closed: a malformed head, no matching mount,
557/// or an un-dispatchable nested target ⇒ 404/drop. For a matched nested `Proxy`, the request head consumed
558/// here is replayed to the backend first (via [`proxy_to_backend_with_prefix`]) so the backend sees
559/// the complete request. Backend dial failures inside a nested `Proxy` drop the conn. Nested `Path`
560/// is rejected by `ServeState::validate`, so it is not expected here; it is dropped fail-closed if it
561/// ever reaches dispatch.
562async fn serve_path<S>(port: u16, mut tls: S, handlers: &BTreeMap<String, ServeTarget>)
563where
564    S: AsyncRead + AsyncWrite + Unpin,
565{
566    let Some((buf, _end)) = read_http_head(&mut tls).await else {
567        tracing::debug!(%port, "serve path: incomplete/oversized request head; dropping conn");
568        return;
569    };
570    let Some(path) = request_path(&buf) else {
571        write_http_status(port, tls, "400 Bad Request").await;
572        return;
573    };
574
575    let Some(target) = match_path_handler(handlers, &path) else {
576        write_http_status(port, tls, "404 Not Found").await;
577        return;
578    };
579
580    match target {
581        // The request head was already consumed off `tls` by `read_http_head`; replay it (`buf`) to
582        // the backend FIRST so the backend sees the complete request (head + remaining body/stream),
583        // not a request with its first request-line+headers missing.
584        ServeTarget::Proxy { to } => proxy_to_backend_with_prefix(port, tls, to, &buf).await,
585        ServeTarget::Text { body } => write_text(port, tls, body).await,
586        ServeTarget::Redirect { to, status } => serve_redirect(port, tls, to, *status).await,
587        // Accept (no hand-back channel here), TcpForward (raw, not on a TLS path), nested Path
588        // (rejected by validate), and any future `#[non_exhaustive]` variant are not servable as a
589        // Path leaf: drop fail-closed rather than guess.
590        _ => {
591            tracing::warn!(%port, "serve path: unsupported nested target; dropping conn");
592            write_http_status(port, tls, "404 Not Found").await;
593        }
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600
601    fn proxy(to: &str) -> ServeTarget {
602        ServeTarget::Proxy { to: to.into() }
603    }
604
605    #[test]
606    fn cap_is_bounded() {
607        assert_eq!(MAX_SERVE_CONNS_PER_PORT, 256);
608    }
609
610    #[test]
611    fn reconcile_adds_new_ports() {
612        let current = BTreeMap::new();
613        let mut next = BTreeMap::new();
614        next.insert(443u16, ServeTarget::Accept);
615        next.insert(8443u16, proxy("127.0.0.1:8080"));
616        let (add, remove) = pure_reconcile(&current, &next);
617        assert_eq!(add, BTreeSet::from([443, 8443]));
618        assert!(remove.is_empty());
619    }
620
621    #[test]
622    fn reconcile_removes_dropped_ports() {
623        let mut current = BTreeMap::new();
624        current.insert(443u16, ServeTarget::Accept);
625        current.insert(8443u16, proxy("127.0.0.1:8080"));
626        let mut next = BTreeMap::new();
627        next.insert(443u16, ServeTarget::Accept);
628        let (add, remove) = pure_reconcile(&current, &next);
629        assert!(add.is_empty());
630        assert_eq!(remove, BTreeSet::from([8443]));
631    }
632
633    #[test]
634    fn reconcile_changed_port_is_remove_and_add() {
635        // Same port, different target => counts as both (full-replace would respawn it anyway).
636        let mut current = BTreeMap::new();
637        current.insert(443u16, proxy("127.0.0.1:8080"));
638        let mut next = BTreeMap::new();
639        next.insert(443u16, proxy("127.0.0.1:9090"));
640        let (add, remove) = pure_reconcile(&current, &next);
641        assert_eq!(add, BTreeSet::from([443]));
642        assert_eq!(remove, BTreeSet::from([443]));
643    }
644
645    #[test]
646    fn reconcile_unchanged_port_is_noop() {
647        let mut current = BTreeMap::new();
648        current.insert(443u16, ServeTarget::Accept);
649        let next = current.clone();
650        let (add, remove) = pure_reconcile(&current, &next);
651        assert!(add.is_empty());
652        assert!(remove.is_empty());
653    }
654
655    #[test]
656    fn terminates_tls_matches_dispatch_arm() {
657        // The dispatch decision (TLS vs raw) must agree with the type's own `terminates_tls`: only
658        // TcpForward is raw; Accept/Proxy/Text/Path/Redirect all terminate TLS.
659        assert!(ServeTarget::Accept.terminates_tls());
660        assert!(proxy("127.0.0.1:8080").terminates_tls());
661        assert!(ServeTarget::Text { body: "ok".into() }.terminates_tls());
662        assert!(
663            ServeTarget::Redirect {
664                to: "/elsewhere".into(),
665                status: 302,
666            }
667            .terminates_tls()
668        );
669        let mut handlers = BTreeMap::new();
670        handlers.insert("/".to_string(), proxy("127.0.0.1:8080"));
671        assert!(ServeTarget::Path { handlers }.terminates_tls());
672        assert!(
673            !ServeTarget::TcpForward {
674                to: "127.0.0.1:5000".into()
675            }
676            .terminates_tls()
677        );
678    }
679
680    #[test]
681    fn find_header_end_shared_with_peerapi_doh() {
682        // The local mirror was removed; serve dispatch now uses the shared peerAPI helper. Keep one
683        // assertion that the shared fn behaves as serve dispatch relies on (peerapi_doh owns the
684        // exhaustive coverage).
685        assert_eq!(
686            crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n\r\n"),
687            Some(18)
688        );
689        assert_eq!(
690            crate::peerapi_doh::find_header_end(b"GET / HTTP/1.1\r\n"),
691            None
692        );
693    }
694
695    #[test]
696    fn request_path_strips_query() {
697        assert_eq!(
698            request_path(b"GET /api/v1?x=1 HTTP/1.1\r\nHost: h\r\n\r\n").as_deref(),
699            Some("/api/v1")
700        );
701        assert_eq!(
702            request_path(b"GET / HTTP/1.1\r\n\r\n").as_deref(),
703            Some("/")
704        );
705        assert_eq!(request_path(b"not a request").as_deref(), None);
706    }
707
708    #[test]
709    fn request_path_none_on_malformed_request_line() {
710        // No method/version framing at all => httparse rejects => None.
711        assert_eq!(request_path(b"GARBAGE\r\n\r\n").as_deref(), None);
712        // Empty buffer => incomplete => None.
713        assert_eq!(request_path(b"").as_deref(), None);
714    }
715
716    /// The mux `serve_path` dispatch tests below route against: root, `/api`, `/api/v2`, each with a
717    /// distinguishable backend so a test can assert which one a path did *not* reach.
718    fn mux() -> BTreeMap<String, ServeTarget> {
719        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
720        handlers.insert("/".to_string(), proxy("127.0.0.1:1"));
721        handlers.insert("/api".to_string(), proxy("127.0.0.1:2"));
722        handlers.insert("/api/v2".to_string(), proxy("127.0.0.1:3"));
723        handlers
724    }
725
726    #[test]
727    fn longest_matching_mount_wins() {
728        // Calls the production selection (`serve_path` calls the same fn) — not a copy of it.
729        let handlers = mux();
730        assert_eq!(
731            match_path_handler(&handlers, "/api/v2/x"),
732            Some(&proxy("127.0.0.1:3")),
733            "the longest mount claiming the path must win"
734        );
735        assert_eq!(
736            match_path_handler(&handlers, "/api/v1"),
737            Some(&proxy("127.0.0.1:2"))
738        );
739        assert_eq!(
740            match_path_handler(&handlers, "/api"),
741            Some(&proxy("127.0.0.1:2"))
742        );
743        assert_eq!(
744            match_path_handler(&handlers, "/other"),
745            Some(&proxy("127.0.0.1:1"))
746        );
747    }
748
749    #[test]
750    fn mount_does_not_claim_a_longer_first_segment() {
751        // The negative case, and the whole point: a `/api` mount must NOT swallow `/apifoo`. A raw
752        // byte-prefix test routes these to the `/api` backend; Go's segment-boundary lookup does
753        // not, and neither may we. Assert where they must *not* go, not only where they must.
754        let handlers = mux();
755        let api = proxy("127.0.0.1:2");
756        let root = proxy("127.0.0.1:1");
757        for path in ["/apifoo", "/apibar", "/api-internal", "/api_v2", "/apis/x"] {
758            let picked = match_path_handler(&handlers, path);
759            assert_ne!(picked, Some(&api), "{path} must not reach the /api backend");
760            assert_eq!(
761                picked,
762                Some(&root),
763                "{path} must fall through to the / mount"
764            );
765        }
766        // Same shape one level down: `/api/v2` must not claim `/api/v20`.
767        let picked = match_path_handler(&handlers, "/api/v20");
768        assert_ne!(
769            picked,
770            Some(&proxy("127.0.0.1:3")),
771            "/api/v20 must not reach the /api/v2 backend"
772        );
773        assert_eq!(picked, Some(&api));
774    }
775
776    #[test]
777    fn mount_claims_itself_and_paths_below_it() {
778        assert!(mount_claims_path("/api", "/api"));
779        assert!(mount_claims_path("/api", "/api/"));
780        assert!(mount_claims_path("/api", "/api/v2/x"));
781        assert!(!mount_claims_path("/api", "/apifoo"));
782        assert!(!mount_claims_path("/api", "/ap"));
783        assert!(!mount_claims_path("/api", "/"));
784        // The root mount claims everything.
785        assert!(mount_claims_path("/", "/"));
786        assert!(mount_claims_path("/", "/anything/at/all"));
787    }
788
789    #[test]
790    fn trailing_slash_mount_needs_no_doubled_slash() {
791        // `/api/` is the same mount as `/api`: it claims `/api/v2`, not only `/api//v2`.
792        assert!(mount_claims_path("/api/", "/api/v2"));
793        assert!(mount_claims_path("/api/", "/api/"));
794        assert!(mount_claims_path("/api/", "/api"));
795        assert!(!mount_claims_path("/api/", "/apifoo"));
796
797        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
798        handlers.insert("/".to_string(), proxy("127.0.0.1:1"));
799        handlers.insert("/api/".to_string(), proxy("127.0.0.1:2"));
800        assert_eq!(
801            match_path_handler(&handlers, "/api/v2"),
802            Some(&proxy("127.0.0.1:2"))
803        );
804        assert_eq!(
805            match_path_handler(&handlers, "/apifoo"),
806            Some(&proxy("127.0.0.1:1")),
807            "/apifoo must fall through to / even when the mount is spelled /api/"
808        );
809    }
810
811    #[test]
812    fn unmatched_path_selects_nothing() {
813        // No root mount => a path no mount claims is `None`, which dispatch turns into a 404.
814        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
815        handlers.insert("/api".to_string(), proxy("127.0.0.1:2"));
816        assert_eq!(match_path_handler(&handlers, "/apifoo"), None);
817        assert_eq!(match_path_handler(&handlers, "/other"), None);
818        assert_eq!(
819            match_path_handler(&handlers, "/api/v2"),
820            Some(&proxy("127.0.0.1:2"))
821        );
822    }
823
824    #[test]
825    fn redirect_reason_known_statuses() {
826        assert_eq!(redirect_reason(301), "Moved Permanently");
827        assert_eq!(redirect_reason(308), "Permanent Redirect");
828        assert_eq!(redirect_reason(399), "Redirect");
829    }
830
831    use tokio::io::{AsyncReadExt, AsyncWriteExt};
832
833    /// Read everything the server side wrote to the `client` half of a duplex until the server task
834    /// closes its end (drop/shutdown), returning it as a `String`.
835    async fn drain_to_string(mut client: tokio::io::DuplexStream) -> String {
836        let mut out = Vec::new();
837        drop(client.read_to_end(&mut out).await);
838        String::from_utf8(out).expect("server emitted valid utf8")
839    }
840
841    #[tokio::test]
842    async fn serve_redirect_emits_exact_response() {
843        let (client, server) = tokio::io::duplex(4096);
844        let t = tokio::spawn(async move {
845            serve_redirect(443, server, "/elsewhere", 302).await;
846        });
847        let got = drain_to_string(client).await;
848        t.await.unwrap();
849        assert_eq!(
850            got,
851            "HTTP/1.1 302 Found\r\nLocation: /elsewhere\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
852        );
853    }
854
855    #[tokio::test]
856    async fn write_http_status_emits_status_line() {
857        let (client, server) = tokio::io::duplex(4096);
858        let t = tokio::spawn(async move {
859            write_http_status(443, server, "404 Not Found").await;
860        });
861        let got = drain_to_string(client).await;
862        t.await.unwrap();
863        assert_eq!(
864            got,
865            "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
866        );
867
868        let (client, server) = tokio::io::duplex(4096);
869        let t = tokio::spawn(async move {
870            write_http_status(443, server, "400 Bad Request").await;
871        });
872        let got = drain_to_string(client).await;
873        t.await.unwrap();
874        assert_eq!(
875            got,
876            "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
877        );
878    }
879
880    #[tokio::test]
881    async fn read_http_head_reads_terminated_head() {
882        let (mut client, mut server) = tokio::io::duplex(4096);
883        client
884            .write_all(b"GET /api HTTP/1.1\r\nHost: h\r\n\r\nBODY")
885            .await
886            .unwrap();
887        drop(client);
888        let (buf, end) = read_http_head(&mut server).await.expect("complete head");
889        // `end` points just past the terminator; the head + trailing body are both buffered.
890        assert_eq!(&buf[..end], b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n");
891        assert_eq!(&buf[end..], b"BODY");
892    }
893
894    #[tokio::test]
895    async fn read_http_head_none_on_early_eof() {
896        let (mut client, mut server) = tokio::io::duplex(4096);
897        client.write_all(b"GET / HTTP/1.1\r\n").await.unwrap();
898        drop(client); // EOF before the terminator
899        assert!(read_http_head(&mut server).await.is_none());
900    }
901
902    #[tokio::test]
903    async fn read_http_head_none_on_oversized_head() {
904        let (mut client, mut server) = tokio::io::duplex(64 * 1024);
905        // A head that never terminates and exceeds MAX_HTTP_HEAD must be dropped fail-closed.
906        let oversized = vec![b'a'; MAX_HTTP_HEAD + 1024];
907        client.write_all(&oversized).await.unwrap();
908        drop(client);
909        assert!(read_http_head(&mut server).await.is_none());
910    }
911
912    #[tokio::test]
913    async fn read_http_head_never_exceeds_max_head() {
914        // A terminator landing exactly at the bound still succeeds (the buffer never overshoots).
915        let (mut client, mut server) = tokio::io::duplex(MAX_HTTP_HEAD + 16);
916        let mut head = vec![b'a'; MAX_HTTP_HEAD - 4];
917        head.extend_from_slice(b"\r\n\r\n");
918        assert_eq!(head.len(), MAX_HTTP_HEAD);
919        client.write_all(&head).await.unwrap();
920        drop(client);
921        let (buf, end) = read_http_head(&mut server).await.expect("head at bound");
922        assert_eq!(end, MAX_HTTP_HEAD);
923        assert!(buf.len() <= MAX_HTTP_HEAD);
924    }
925
926    #[tokio::test]
927    async fn proxy_with_prefix_writes_prefix_before_bidi_copy() {
928        // Fix 1 regression guard: the consumed request head MUST hit the backend FIRST, before the
929        // bidirectional splice forwards the rest of the client stream. The backend is a real
930        // loopback TcpListener (the helper dials `to` via tokio TcpStream).
931        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
932        let backend_addr = listener.local_addr().unwrap();
933
934        let prefix = b"GET /api HTTP/1.1\r\nHost: h\r\n\r\n";
935        let body = b"trailing-body-bytes";
936        let backend = tokio::spawn(async move {
937            let (mut sock, _) = listener.accept().await.unwrap();
938            let mut head = vec![0u8; prefix.len()];
939            sock.read_exact(&mut head).await.unwrap();
940            let mut rest = vec![0u8; body.len()];
941            sock.read_exact(&mut rest).await.unwrap();
942            (head, rest)
943        });
944
945        // Client side of the duplex stands in for the TLS-terminated stream the helper splices.
946        let (mut client, server) = tokio::io::duplex(4096);
947        let to = backend_addr.to_string();
948        let proxy_task = tokio::spawn(async move {
949            proxy_to_backend_with_prefix(443, server, &to, prefix).await;
950        });
951
952        // Feed the rest of the request body through the splice, then close.
953        client.write_all(body).await.unwrap();
954        drop(client);
955
956        let (head, rest) = backend.await.unwrap();
957        proxy_task.await.unwrap();
958        assert_eq!(
959            head, prefix,
960            "prefix (consumed head) replayed to backend first"
961        );
962        assert_eq!(rest, body, "remaining stream spliced after the prefix");
963    }
964
965    #[tokio::test]
966    async fn serve_path_proxy_replays_consumed_head_to_backend() {
967        // End-to-end longest-prefix selection routing to a nested Proxy: the head consumed by
968        // `read_http_head` must reach the backend, proving the request is not dropped (the bug).
969        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
970        let backend_addr = listener.local_addr().unwrap();
971        let request = b"GET /api/v2/x HTTP/1.1\r\nHost: h\r\n\r\n";
972        let backend = tokio::spawn(async move {
973            let (mut sock, _) = listener.accept().await.unwrap();
974            let mut head = vec![0u8; request.len()];
975            sock.read_exact(&mut head).await.unwrap();
976            head
977        });
978
979        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
980        handlers.insert("/".to_string(), proxy("127.0.0.1:1")); // shorter prefix (not selected)
981        handlers.insert("/api/v2".to_string(), proxy(&backend_addr.to_string())); // longest match
982
983        let (mut client, server) = tokio::io::duplex(4096);
984        let path_task = tokio::spawn(async move {
985            serve_path(443, server, &handlers).await;
986        });
987        client.write_all(request).await.unwrap();
988        drop(client);
989
990        let head = backend.await.unwrap();
991        path_task.await.unwrap();
992        assert_eq!(
993            head, request,
994            "serve_path routed to the longest-prefix Proxy and replayed the consumed head"
995        );
996    }
997
998    #[tokio::test]
999    async fn serve_path_text_target_emits_body() {
1000        // Longest-prefix selection routing to a nested Text target: the body is emitted verbatim.
1001        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
1002        handlers.insert(
1003            "/".to_string(),
1004            ServeTarget::Text {
1005                body: "root".into(),
1006            },
1007        );
1008        handlers.insert(
1009            "/hello".to_string(),
1010            ServeTarget::Text {
1011                body: "hello-body".into(),
1012            },
1013        );
1014
1015        let (mut client, server) = tokio::io::duplex(4096);
1016        let t = tokio::spawn(async move {
1017            serve_path(443, server, &handlers).await;
1018        });
1019        client
1020            .write_all(b"GET /hello/world HTTP/1.1\r\nHost: h\r\n\r\n")
1021            .await
1022            .unwrap();
1023        // Keep the client half open: `read_http_head` already saw the full head, and the Text target
1024        // neither reads further nor needs EOF. Drain the body the server writes + shuts down.
1025        let got = drain_to_string(client).await;
1026        t.await.unwrap();
1027        assert_eq!(got, "hello-body");
1028    }
1029
1030    #[tokio::test]
1031    async fn serve_path_does_not_route_a_longer_first_segment_to_the_shorter_mount() {
1032        // End to end through the real dispatch: with `/` and `/hello` mounted, `/hellofoo` is a
1033        // different path, not a path below `/hello`, so it must be served by the `/` mount.
1034        let mut handlers: BTreeMap<String, ServeTarget> = BTreeMap::new();
1035        handlers.insert(
1036            "/".to_string(),
1037            ServeTarget::Text {
1038                body: "root".into(),
1039            },
1040        );
1041        handlers.insert(
1042            "/hello".to_string(),
1043            ServeTarget::Text {
1044                body: "hello-body".into(),
1045            },
1046        );
1047
1048        let (mut client, server) = tokio::io::duplex(4096);
1049        let t = tokio::spawn(async move {
1050            serve_path(443, server, &handlers).await;
1051        });
1052        client
1053            .write_all(b"GET /hellofoo HTTP/1.1\r\nHost: h\r\n\r\n")
1054            .await
1055            .unwrap();
1056        let got = drain_to_string(client).await;
1057        t.await.unwrap();
1058        assert_ne!(
1059            got, "hello-body",
1060            "/hellofoo must not reach the /hello mount"
1061        );
1062        assert_eq!(got, "root");
1063    }
1064
1065    // NOTE: a live bind+accept test needs a running netstack channel + overlay; the existing
1066    // netstack-backed managers (fallback_tcp) likewise unit-test only the pure pieces (port diff,
1067    // dispatch decision) and leave the bind/accept path to integration coverage. The byte-emission
1068    // helpers above are exercised directly over `tokio::io::duplex` + loopback `TcpStream` backends;
1069    // the bind/accept/splice path is exercised via `Device::set_serve_config` against a real device.
1070}