Skip to main content

microvms_edges/session/
serve.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The serving loops behind `microvm tunnel` and `microvm port-forward`, and behind the SDKs'
3//! tunnel and port-forward handles (#263).
4//!
5//! Each loop accepts a local connection, serves it on a task of its own, and counts how it
6//! ended. The three things the CLI used to own are parameters: a stop future (the CLI's Ctrl-C,
7//! a handle's `stop()`), a limit on how many connections to accept, and a callback that hears
8//! about each connection's end, so a caller can print a warning or keep a list. A connection
9//! that fails never ends the loop; only the stop, the limit, or a listener that stops accepting
10//! does. The report's counts are computed here, once, for every surface.
11//!
12//! # Every connection gets a task, both loops alike
13//!
14//! One WebSocket per tunnel connection matches the daemon's no-multiplexing decision, and one
15//! task per forwarded connection means a slow request (a long poll, a first compile) doesn't
16//! hold the browser's next one behind it, which the CLI's port-forward used to do. When the
17//! loop stops accepting, it waits for the connections already open to end: a task still
18//! relaying holds bytes the local client is waiting for, and dropping it would cut them short.
19//! Only an SDK handle cuts them, when its caller's `stop` gives a grace that runs out or the
20//! handle is dropped.
21//!
22//! # A direct session serves too
23//!
24//! A session with no proxy credential reaches the daemon without the endpoint proxy. The
25//! tunnel then offers no proxy subprotocols, and the daemon's bearer check is the gate; the
26//! forwarder mints no proxy headers and sends each request to the endpoint's host at the guest
27//! port ([`super::forward::direct_url`]), since there is no proxy to route it by header.
28
29use std::future::Future;
30use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
31use std::sync::{Arc, Mutex, PoisonError};
32use std::time::Duration;
33
34use tokio::net::TcpListener;
35use tokio::sync::{oneshot, watch};
36use tokio::task::JoinSet;
37
38use microvms_app::error::{Error, ErrorKind};
39use microvms_app::identity::TunnelIdentity;
40use microvms_app::session::Session;
41use microvms_app::session::proxy::ProxyAuth;
42
43use super::forward::{ForwardClient, ForwardEvent, ForwardSpec, serve_connection_via};
44use super::tunnel::{TunnelEnd, relay_connection_inner};
45
46/// When a loop stops on its own, besides its caller's stop.
47#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub struct ServeLimits {
49    /// Stop accepting after this many connections. `None` serves until stopped.
50    pub max_connections: Option<u32>,
51}
52
53/// Why a loop stopped accepting.
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub enum StopReason {
56    /// The caller's stop future resolved.
57    Requested,
58    /// [`ServeLimits::max_connections`] connections were accepted.
59    Limit,
60    /// The listener stopped accepting, with why. A listener's failure isn't one connection's,
61    /// so the loop ends rather than retrying a socket that will keep failing.
62    ListenerFailed(String),
63}
64
65impl StopReason {
66    /// Whether the caller stopped the loop, which the CLI reports as `interrupted`: a stop is
67    /// a success, and a loop that ended at its limit or on its listener wasn't interrupted.
68    pub fn was_requested(&self) -> bool {
69        matches!(self, StopReason::Requested)
70    }
71}
72
73/// What a tunnel reaches: the daemon, the guest port, and the credentials to present.
74#[derive(Clone)]
75pub struct TunnelTarget {
76    pub endpoint: String,
77    pub agent_token: String,
78    /// The session's proxy credential; `None` for a direct session.
79    pub auth: Option<Arc<ProxyAuth>>,
80    pub guest_port: u16,
81    /// The pair to verify the far end with (`--verify-identity`), when asked for.
82    pub identity: Option<TunnelIdentity>,
83}
84
85impl TunnelTarget {
86    /// The target for `guest_port` over `session`'s endpoint and credentials.
87    pub fn for_session(
88        session: &Session,
89        guest_port: u16,
90        identity: Option<TunnelIdentity>,
91    ) -> Self {
92        Self {
93            endpoint: session.endpoint().to_string(),
94            agent_token: session.agent_token().to_string(),
95            auth: session.proxy_auth().cloned(),
96            guest_port,
97            identity,
98        }
99    }
100}
101
102/// How one tunnel connection ended.
103#[derive(Debug)]
104pub struct TunnelConnection {
105    pub peer: SocketAddr,
106    pub end: Result<TunnelEnd, Error>,
107}
108
109/// What a tunnel loop did.
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct TunnelReport {
112    /// Connections accepted.
113    pub served: u32,
114    /// Connections the daemon refused, or that failed with an error.
115    pub refused: u32,
116    /// Verified connections that ended without the daemon's end of stream (#342).
117    pub truncated: u32,
118    /// Verified connections whose daemon predates the end of stream (#342).
119    pub unproven: u32,
120    /// Proxy tokens the session minted by the time the loop ended.
121    pub proxy_token_mints: u64,
122    pub stopped: StopReason,
123}
124
125impl TunnelReport {
126    /// Counts one connection's end in its column. A clean end is only served.
127    fn count(&mut self, end: &Result<TunnelEnd, Error>) {
128        match end {
129            Ok(TunnelEnd::Closed) => {}
130            Ok(TunnelEnd::ClosedUnproven) => self.unproven += 1,
131            Ok(TunnelEnd::Truncated { .. }) => self.truncated += 1,
132            Ok(TunnelEnd::Refused { .. }) | Err(_) => self.refused += 1,
133        }
134    }
135}
136
137/// Serves `listener` as a TCP tunnel to `target` until `stop` resolves, the limit is reached,
138/// or the listener fails, then waits for the open connections to end.
139///
140/// `on_end` hears about each connection as it ends, on the loop's own task.
141pub async fn serve_tunnel<F>(
142    listener: TcpListener,
143    target: TunnelTarget,
144    limits: ServeLimits,
145    stop: impl Future<Output = ()>,
146    on_end: F,
147) -> TunnelReport
148where
149    F: FnMut(TunnelConnection),
150{
151    tunnel_loop(
152        listener,
153        target,
154        limits,
155        stop,
156        std::future::pending(),
157        on_end,
158    )
159    .await
160}
161
162/// [`serve_tunnel`], cutting the connections still open once `cut` resolves after the stop.
163async fn tunnel_loop<F>(
164    listener: TcpListener,
165    target: TunnelTarget,
166    limits: ServeLimits,
167    stop: impl Future<Output = ()>,
168    cut: impl Future<Output = ()>,
169    mut on_end: F,
170) -> TunnelReport
171where
172    F: FnMut(TunnelConnection),
173{
174    let target = Arc::new(target);
175    let mut report = TunnelReport {
176        served: 0,
177        refused: 0,
178        truncated: 0,
179        unproven: 0,
180        proxy_token_mints: 0,
181        stopped: StopReason::Requested,
182    };
183    let (stopped, served) = serve(
184        &listener,
185        limits,
186        stop,
187        cut,
188        |mut stream, _| {
189            let target = Arc::clone(&target);
190            async move {
191                let result = relay_connection_inner(
192                    &mut stream,
193                    &target.endpoint,
194                    target.guest_port,
195                    &target.agent_token,
196                    target.auth.as_ref(),
197                    target.identity.as_ref(),
198                )
199                .await;
200                // A verified stream only ends cleanly when its authenticated end arrives.
201                // Keep the socket until the verdict so a short stream reaches the local
202                // application as a reset, after any bytes already delivered.
203                if target.identity.is_some()
204                    && matches!(
205                        result,
206                        Err(_) | Ok(TunnelEnd::Truncated { .. } | TunnelEnd::Refused { .. })
207                    )
208                {
209                    stream.set_zero_linger().map_err(|error| {
210                        Error::new(
211                            ErrorKind::Unexpected,
212                            format!("could not reset the local tunnel connection: {error}"),
213                        )
214                    })?;
215                }
216                result
217            }
218        },
219        |peer, end| {
220            report.count(&end);
221            on_end(TunnelConnection { peer, end });
222        },
223    )
224    .await;
225    report.served = served;
226    report.stopped = stopped;
227    report.proxy_token_mints = target.auth.as_ref().map_or(0, |auth| auth.mint_count());
228    report
229}
230
231/// Something one forwarded connection did, for a caller's warnings.
232#[derive(Debug)]
233pub enum ForwardNotice {
234    /// An event the forwarder reported for the connection from `peer`.
235    Event {
236        peer: SocketAddr,
237        event: ForwardEvent,
238    },
239    /// The connection from `peer` ended early, with why.
240    Failed { peer: SocketAddr, error: Error },
241}
242
243/// What a port-forward loop did.
244#[derive(Clone, Debug, Eq, PartialEq)]
245pub struct ForwardReport {
246    /// Connections accepted.
247    pub served: u32,
248    /// Exchanges the proxy refused ([`ForwardEvent::Refused`]).
249    pub refused: u32,
250    /// Exchanges that upgraded, a WebSocket among them.
251    pub upgrades: u32,
252    /// Proxy tokens the session minted by the time the loop ended.
253    pub proxy_token_mints: u64,
254    pub stopped: StopReason,
255}
256
257impl ForwardReport {
258    /// Counts one of a connection's events in its column.
259    fn count(&mut self, event: &ForwardEvent) {
260        match event {
261            ForwardEvent::Refused { .. } => self.refused += 1,
262            ForwardEvent::Forwarded { upgraded: true, .. } => self.upgrades += 1,
263            _ => {}
264        }
265    }
266}
267
268/// Serves `listener` as an HTTP and WebSocket forward to `spec`'s guest port until `stop`
269/// resolves, the limit is reached, or the listener fails, then waits for the open connections
270/// to end.
271///
272/// `auth` is the session's proxy credential, `None` for a direct session. `on_notice` hears
273/// about each connection's events and failure as it ends, on the loop's own task.
274pub async fn serve_forward<F>(
275    listener: TcpListener,
276    spec: ForwardSpec,
277    auth: Option<Arc<ProxyAuth>>,
278    limits: ServeLimits,
279    stop: impl Future<Output = ()>,
280    on_notice: F,
281) -> Result<ForwardReport, Error>
282where
283    F: FnMut(ForwardNotice),
284{
285    forward_loop(
286        listener,
287        spec,
288        auth,
289        limits,
290        stop,
291        std::future::pending(),
292        on_notice,
293    )
294    .await
295}
296
297/// [`serve_forward`], cutting the connections still open once `cut` resolves after the stop.
298async fn forward_loop<F>(
299    listener: TcpListener,
300    spec: ForwardSpec,
301    auth: Option<Arc<ProxyAuth>>,
302    limits: ServeLimits,
303    stop: impl Future<Output = ()>,
304    cut: impl Future<Output = ()>,
305    mut on_notice: F,
306) -> Result<ForwardReport, Error>
307where
308    F: FnMut(ForwardNotice),
309{
310    // One client for the whole forward: connection reuse to the endpoint is what keeps a
311    // page-load of thirty assets from paying thirty TLS handshakes.
312    let client = Arc::new(ForwardClient::new()?);
313    let mut report = ForwardReport {
314        served: 0,
315        refused: 0,
316        upgrades: 0,
317        proxy_token_mints: 0,
318        stopped: StopReason::Requested,
319    };
320    let (stopped, served) = serve(
321        &listener,
322        limits,
323        stop,
324        cut,
325        |stream, _| {
326            let (mut spec, auth, client) = (spec.clone(), auth.clone(), Arc::clone(&client));
327            async move {
328                // A wildcard listener has no single address. The accepted socket names
329                // the concrete local interface, including the port chosen for a :0 bind.
330                spec.bind = stream.local_addr().map_err(|error| {
331                    Error::new(
332                        ErrorKind::Unexpected,
333                        format!("reading the local forward address failed: {error}"),
334                    )
335                })?;
336                let mut events = Vec::new();
337                let outcome =
338                    serve_connection_via(stream, &spec, auth.as_ref(), &client, |event| {
339                        events.push(event);
340                    })
341                    .await;
342                Ok((events, outcome))
343            }
344        },
345        |peer, done| {
346            let (events, failure) = match done {
347                Ok((events, outcome)) => (events, outcome.err()),
348                Err(error) => (Vec::new(), Some(error)),
349            };
350            for event in events {
351                report.count(&event);
352                on_notice(ForwardNotice::Event { peer, event });
353            }
354            if let Some(error) = failure {
355                on_notice(ForwardNotice::Failed { peer, error });
356            }
357        },
358    )
359    .await;
360    report.served = served;
361    report.stopped = stopped;
362    report.proxy_token_mints = auth.as_ref().map_or(0, |auth| auth.mint_count());
363    Ok(report)
364}
365
366// ── the SDKs' handles ────────────────────────────────────────────────────────
367
368/// Where an SDK's tunnel or forward listens unless told otherwise: loopback, on a port the OS
369/// picks. Loopback because a forward carries the VM's credentials to whoever connects.
370pub const DEFAULT_BIND: SocketAddr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
371
372/// A local address to listen on: `bind` when given, [`DEFAULT_BIND`] otherwise.
373pub fn bind_address(bind: Option<&str>) -> Result<SocketAddr, Error> {
374    match bind {
375        None => Ok(DEFAULT_BIND),
376        Some(bind) => bind.parse().map_err(|err| {
377            Error::new(
378                ErrorKind::InvalidArg,
379                format!("{bind:?} is not a local address to listen on, such as 127.0.0.1:0: {err}"),
380            )
381        }),
382    }
383}
384
385/// How a connection that didn't end clean ended, as a handle's report lists it.
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387pub enum EndKind {
388    /// The daemon refused the tunnel, or the proxy refused the exchange.
389    Refused,
390    /// A verified tunnel ended without the daemon's end of stream (#342).
391    Truncated,
392    /// A verified tunnel ended into a daemon from before the end of stream (#342).
393    Unproven,
394    /// The connection failed with an error.
395    Failed,
396}
397
398impl EndKind {
399    /// The wire spelling the bindings report.
400    pub fn as_str(self) -> &'static str {
401        match self {
402            EndKind::Refused => "refused",
403            EndKind::Truncated => "truncated",
404            EndKind::Unproven => "unproven",
405            EndKind::Failed => "failed",
406        }
407    }
408}
409
410/// One connection that didn't end clean. A clean end is only counted, so a long-lived handle's
411/// report grows with its failures, not with its traffic.
412#[derive(Clone, Debug, Eq, PartialEq)]
413pub struct ConnectionEnd {
414    pub peer: SocketAddr,
415    pub kind: EndKind,
416    /// The close code or the HTTP status, when the end carried one.
417    pub code: Option<u16>,
418    /// The daemon's reason, the forwarder's explanation, or the error.
419    pub detail: String,
420}
421
422impl ConnectionEnd {
423    /// A tunnel connection's end, unless it was clean.
424    pub fn of_tunnel(connection: &TunnelConnection) -> Option<Self> {
425        let (kind, code, detail) = match &connection.end {
426            Ok(TunnelEnd::Closed) => return None,
427            Ok(TunnelEnd::ClosedUnproven) => (EndKind::Unproven, None, String::new()),
428            Ok(TunnelEnd::Truncated { code }) => (EndKind::Truncated, *code, String::new()),
429            Ok(TunnelEnd::Refused { code, reason }) => {
430                (EndKind::Refused, Some(*code), reason.clone())
431            }
432            Err(error) => (EndKind::Failed, None, error.to_string()),
433        };
434        Some(Self {
435            peer: connection.peer,
436            kind,
437            code,
438            detail,
439        })
440    }
441
442    /// A forwarded connection's refusal or failure; its other events aren't ends worth listing.
443    pub fn of_forward(notice: &ForwardNotice) -> Option<Self> {
444        match notice {
445            ForwardNotice::Event {
446                peer,
447                event:
448                    ForwardEvent::Refused {
449                        status,
450                        explanation,
451                    },
452            } => Some(Self {
453                peer: *peer,
454                kind: EndKind::Refused,
455                code: Some(*status),
456                detail: explanation.clone(),
457            }),
458            ForwardNotice::Event { .. } => None,
459            ForwardNotice::Failed { peer, error } => Some(Self {
460                peer: *peer,
461                kind: EndKind::Failed,
462                code: None,
463                detail: error.to_string(),
464            }),
465        }
466    }
467}
468
469/// What a tunnel handle did: the loop's report, and each connection that didn't end clean.
470#[derive(Clone, Debug, Eq, PartialEq)]
471pub struct TunnelSummary {
472    pub report: TunnelReport,
473    pub ended: Vec<ConnectionEnd>,
474}
475
476/// What a port-forward handle did: the loop's report, and each connection that didn't end
477/// clean.
478#[derive(Clone, Debug, Eq, PartialEq)]
479pub struct ForwardSummary {
480    pub report: ForwardReport,
481    pub ended: Vec<ConnectionEnd>,
482}
483
484type Outcome<S> = Result<S, (ErrorKind, String)>;
485
486/// A serving loop on a background task: the SDKs' tunnel and port-forward handles.
487///
488/// Dropping it stops the loop and cuts the connections still open, so no relay outlives the
489/// handle that owns it: a relay carries the VM's credentials to whoever connected.
490pub struct Serving<S> {
491    local: SocketAddr,
492    stop: Mutex<Option<oneshot::Sender<()>>>,
493    cut: Mutex<Option<oneshot::Sender<()>>>,
494    outcome: Arc<Mutex<Option<Outcome<S>>>>,
495    done: watch::Receiver<bool>,
496}
497
498impl<S: Clone + Send + 'static> Serving<S> {
499    /// Runs `serving` on the current tokio runtime, handing it the stop and the cut futures.
500    /// Panics outside a runtime, like `tokio::spawn`.
501    fn spawn<Fut>(
502        local: SocketAddr,
503        serving: impl FnOnce(oneshot::Receiver<()>, oneshot::Receiver<()>) -> Fut,
504    ) -> Self
505    where
506        Fut: Future<Output = Result<S, Error>> + Send + 'static,
507    {
508        let (stop_tx, stop_rx) = oneshot::channel();
509        let (cut_tx, cut_rx) = oneshot::channel();
510        let (done_tx, done_rx) = watch::channel(false);
511        let outcome = Arc::new(Mutex::new(None));
512        let written = Arc::clone(&outcome);
513        let serving = serving(stop_rx, cut_rx);
514        tokio::spawn(async move {
515            let result = serving
516                .await
517                .map_err(|error| (error.kind(), error.to_string()));
518            *written.lock().unwrap_or_else(PoisonError::into_inner) = Some(result);
519            done_tx.send_replace(true);
520        });
521        Self {
522            local,
523            stop: Mutex::new(Some(stop_tx)),
524            cut: Mutex::new(Some(cut_tx)),
525            outcome,
526            done: done_rx,
527        }
528    }
529
530    /// The address the loop listens on, with the port the OS picked for a port 0.
531    pub fn local_address(&self) -> SocketAddr {
532        self.local
533    }
534
535    /// Whether the loop is still serving, or waiting for its open connections to end.
536    pub fn is_running(&self) -> bool {
537        !*self.done.borrow()
538    }
539
540    /// Asks the loop to stop accepting. Idempotent; [`Self::finished`] waits for it, and for the
541    /// connections still open.
542    pub fn request_stop(&self) {
543        signal(&self.stop);
544    }
545
546    /// Stops accepting and waits for the connections still open to end, cutting the ones
547    /// still open after `grace` when it's given, and returns what the loop did.
548    ///
549    /// Without a grace, a client that keeps its connection open keeps this waiting: the
550    /// relay holds bytes that client may still be reading, so the loop won't cut it unasked.
551    pub async fn stop(&self, grace: Option<Duration>) -> Result<S, Error> {
552        self.request_stop();
553        if let Some(grace) = grace {
554            if let Ok(outcome) = tokio::time::timeout(grace, self.finished()).await {
555                return outcome;
556            }
557            signal(&self.cut);
558        }
559        self.finished().await
560    }
561
562    /// Waits for the loop to end and returns what it did. Callable any number of times.
563    pub async fn finished(&self) -> Result<S, Error> {
564        let mut done = self.done.clone();
565        // The sender drops only after writing the outcome, so an error here still means done.
566        let _ = done.wait_for(|finished| *finished).await;
567        self.outcome
568            .lock()
569            .unwrap_or_else(PoisonError::into_inner)
570            .clone()
571            .expect("the loop records its outcome before signalling done")
572            .map_err(|(kind, message)| Error::new(kind, message))
573    }
574}
575
576/// Sends a handle's stop or cut, once.
577fn signal(sender: &Mutex<Option<oneshot::Sender<()>>>) {
578    if let Some(sender) = sender.lock().unwrap_or_else(PoisonError::into_inner).take() {
579        let _ = sender.send(());
580    }
581}
582
583/// A handle's stop or cut future: the sender's `send`, or its drop.
584async fn signalled(signal: oneshot::Receiver<()>) {
585    let _ = signal.await;
586}
587
588/// Binds `bind` and starts a tunnel to `target` on the current tokio runtime: the SDKs'
589/// `Session.tunnel`.
590pub async fn start_tunnel(
591    bind: SocketAddr,
592    target: TunnelTarget,
593    limits: ServeLimits,
594) -> Result<Serving<TunnelSummary>, Error> {
595    let listener =
596        super::forward::bind(&ForwardSpec::new(bind, target.guest_port, &target.endpoint)).await?;
597    let local = local_address(&listener)?;
598    Ok(Serving::spawn(local, move |stop, cut| async move {
599        let mut ended = Vec::new();
600        let report = tunnel_loop(
601            listener,
602            target,
603            limits,
604            signalled(stop),
605            signalled(cut),
606            |connection| ended.extend(ConnectionEnd::of_tunnel(&connection)),
607        )
608        .await;
609        Ok(TunnelSummary { report, ended })
610    }))
611}
612
613/// Binds `bind` and starts a port-forward to `guest_port` behind `endpoint` on the current
614/// tokio runtime: the SDKs' `Session.port_forward`. `auth` is `None` for a direct session.
615pub async fn start_forward(
616    bind: SocketAddr,
617    endpoint: &str,
618    guest_port: u16,
619    auth: Option<Arc<ProxyAuth>>,
620    limits: ServeLimits,
621) -> Result<Serving<ForwardSummary>, Error> {
622    let spec = ForwardSpec::new(bind, guest_port, endpoint);
623    let listener = super::forward::bind(&spec).await?;
624    let local = local_address(&listener)?;
625    Ok(Serving::spawn(local, move |stop, cut| async move {
626        let mut ended = Vec::new();
627        let report = forward_loop(
628            listener,
629            spec,
630            auth,
631            limits,
632            signalled(stop),
633            signalled(cut),
634            |notice| ended.extend(ConnectionEnd::of_forward(&notice)),
635        )
636        .await?;
637        Ok(ForwardSummary { report, ended })
638    }))
639}
640
641fn local_address(listener: &TcpListener) -> Result<SocketAddr, Error> {
642    listener.local_addr().map_err(|err| {
643        Error::new(
644            ErrorKind::Unexpected,
645            format!("the listener has no address: {err}"),
646        )
647    })
648}
649
650/// The accept loop both serving loops share: accept until stopped, a task per connection, then
651/// wait for the open ones, cutting them if `cut` resolves first. Answers why it stopped and how
652/// many connections it accepted.
653///
654/// `spawn` makes a connection's task. `ended` takes each connection's result as its task ends,
655/// on this task; a task that panicked or was cut reads as an error.
656async fn serve<T, Fut>(
657    listener: &TcpListener,
658    limits: ServeLimits,
659    stop: impl Future<Output = ()>,
660    cut: impl Future<Output = ()>,
661    mut spawn: impl FnMut(tokio::net::TcpStream, SocketAddr) -> Fut,
662    mut ended: impl FnMut(SocketAddr, Result<T, Error>),
663) -> (StopReason, u32)
664where
665    T: Send + 'static,
666    Fut: Future<Output = Result<T, Error>> + Send + 'static,
667{
668    let mut stop = std::pin::pin!(stop);
669    let mut tasks = JoinSet::new();
670    // Peers by task, so a task that panics is still reported against its connection.
671    let mut peers = Peers::new();
672    type Peers = std::collections::HashMap<tokio::task::Id, SocketAddr>;
673    let mut finish =
674        |peers: &mut Peers,
675         joined: Result<(tokio::task::Id, Result<T, Error>), tokio::task::JoinError>| {
676            let (id, result) = match joined {
677                Ok((id, result)) => (id, result),
678                Err(error) if error.is_cancelled() => (
679                    error.id(),
680                    Err(Error::new(
681                        ErrorKind::Unexpected,
682                        "the connection was cut when its loop stopped",
683                    )),
684                ),
685                Err(error) => (
686                    error.id(),
687                    Err(Error::new(
688                        ErrorKind::Unexpected,
689                        format!("the connection's task ended abnormally: {error}"),
690                    )),
691                ),
692            };
693            let peer = peers
694                .remove(&id)
695                .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0)));
696            ended(peer, result);
697        };
698    let mut served = 0_u32;
699    let stopped = loop {
700        if limits.max_connections.is_some_and(|max| served >= max) {
701            break StopReason::Limit;
702        }
703        tokio::select! {
704            // Biased so a stop wins over a connection that arrived in the same wakeup: the
705            // caller asked to stop, and serving one more first looks like the stop did nothing.
706            biased;
707            () = &mut stop => break StopReason::Requested,
708            Some(joined) = tasks.join_next_with_id(), if !tasks.is_empty() => finish(&mut peers, joined),
709            next = listener.accept() => match next {
710                Ok((stream, peer)) => {
711                    served += 1;
712                    let handle = tasks.spawn(spawn(stream, peer));
713                    peers.insert(handle.id(), peer);
714                }
715                Err(error) => {
716                    break StopReason::ListenerFailed(format!(
717                        "the local listener stopped accepting: {error}"
718                    ));
719                }
720            },
721        }
722    };
723    // Drained rather than abandoned (see the module docs), until the caller cuts what's left.
724    let mut cut = std::pin::pin!(cut);
725    let mut cutting = false;
726    loop {
727        tokio::select! {
728            joined = tasks.join_next_with_id() => match joined {
729                Some(joined) => finish(&mut peers, joined),
730                None => break,
731            },
732            () = &mut cut, if !cutting => {
733                cutting = true;
734                tasks.abort_all();
735            }
736        }
737    }
738    (stopped, served)
739}
740
741#[cfg(test)]
742mod tests {
743    use super::*;
744
745    fn tunnel_report() -> TunnelReport {
746        TunnelReport {
747            served: 0,
748            refused: 0,
749            truncated: 0,
750            unproven: 0,
751            proxy_token_mints: 0,
752            stopped: StopReason::Requested,
753        }
754    }
755
756    /// Each way a tunnel connection ends lands in its own column, once.
757    #[test]
758    fn each_tunnel_end_is_counted_once_in_its_column() {
759        let mut report = tunnel_report();
760        for end in [
761            Ok(TunnelEnd::Closed),
762            Ok(TunnelEnd::ClosedUnproven),
763            Ok(TunnelEnd::ClosedUnproven),
764            Ok(TunnelEnd::Truncated { code: None }),
765            Ok(TunnelEnd::Truncated { code: Some(1000) }),
766            Ok(TunnelEnd::Truncated { code: Some(1000) }),
767            Ok(TunnelEnd::Refused {
768                code: 4502,
769                reason: String::new(),
770            }),
771            Err(Error::new(ErrorKind::Unexpected, "a transport error")),
772        ] {
773            report.count(&end);
774        }
775        assert_eq!(
776            (report.refused, report.truncated, report.unproven),
777            (2, 3, 2),
778            "{report:?}"
779        );
780    }
781
782    /// Only the caller's own stop reads as requested.
783    #[test]
784    fn only_the_callers_stop_was_requested() {
785        assert!(StopReason::Requested.was_requested());
786        assert!(!StopReason::Limit.was_requested());
787        assert!(!StopReason::ListenerFailed("gone".to_string()).was_requested());
788    }
789
790    /// A refusal and an upgrade each land in their column; other events count nowhere.
791    #[test]
792    fn each_forward_event_is_counted_once_in_its_column() {
793        let mut report = ForwardReport {
794            served: 0,
795            refused: 0,
796            upgrades: 0,
797            proxy_token_mints: 0,
798            stopped: StopReason::Requested,
799        };
800        for event in [
801            ForwardEvent::Refused {
802                status: 502,
803                explanation: String::new(),
804            },
805            ForwardEvent::Forwarded {
806                status: 101,
807                upgraded: true,
808            },
809            ForwardEvent::Forwarded {
810                status: 101,
811                upgraded: true,
812            },
813            ForwardEvent::Forwarded {
814                status: 200,
815                upgraded: false,
816            },
817            ForwardEvent::ConnectionError {
818                detail: String::new(),
819            },
820        ] {
821            report.count(&event);
822        }
823        assert_eq!((report.refused, report.upgrades), (1, 2), "{report:?}");
824    }
825
826    /// Each end but a clean one is listed with its kind, its code and its reason; a forward's
827    /// events other than a refusal aren't ends.
828    #[test]
829    fn each_end_but_a_clean_one_is_listed_with_its_kind() {
830        let peer: SocketAddr = "127.0.0.1:5000".parse().expect("an address");
831        let tunnel = |end| ConnectionEnd::of_tunnel(&TunnelConnection { peer, end });
832        let listed = |end: Option<ConnectionEnd>| {
833            end.map(|end| (end.peer, end.kind.as_str(), end.code, end.detail))
834        };
835        assert_eq!(tunnel(Ok(TunnelEnd::Closed)), None);
836        assert_eq!(
837            listed(tunnel(Ok(TunnelEnd::ClosedUnproven))),
838            Some((peer, "unproven", None, String::new()))
839        );
840        assert_eq!(
841            listed(tunnel(Ok(TunnelEnd::Truncated { code: Some(1006) }))),
842            Some((peer, "truncated", Some(1006), String::new()))
843        );
844        assert_eq!(
845            listed(tunnel(Ok(TunnelEnd::Refused {
846                code: 4502,
847                reason: "nothing is listening".into(),
848            }))),
849            Some((peer, "refused", Some(4502), "nothing is listening".into()))
850        );
851        let failed = listed(tunnel(Err(Error::new(ErrorKind::Unexpected, "a reset"))));
852        assert!(
853            matches!(&failed, Some((_, "failed", None, detail)) if detail.contains("a reset")),
854            "{failed:?}"
855        );
856
857        let forward = |event| ConnectionEnd::of_forward(&ForwardNotice::Event { peer, event });
858        assert_eq!(
859            listed(forward(ForwardEvent::Refused {
860                status: 403,
861                explanation: "out of scope".into(),
862            })),
863            Some((peer, "refused", Some(403), "out of scope".into()))
864        );
865        assert_eq!(
866            forward(ForwardEvent::Forwarded {
867                status: 200,
868                upgraded: false,
869            }),
870            None
871        );
872        let failed = listed(ConnectionEnd::of_forward(&ForwardNotice::Failed {
873            peer,
874            error: Error::new(ErrorKind::Unexpected, "a reset"),
875        }));
876        assert!(
877            matches!(&failed, Some((_, "failed", None, detail)) if detail.contains("a reset")),
878            "{failed:?}"
879        );
880    }
881
882    /// A connection's task that panics reads as ended abnormally, and one the caller cuts
883    /// reads as cut: the two are the caller's to tell apart.
884    #[tokio::test]
885    async fn a_panicked_connection_and_a_cut_one_are_told_apart() {
886        let listener = TcpListener::bind("127.0.0.1:0").await.expect("a free port");
887        let local = listener.local_addr().expect("bound");
888        let (cut, cut_rx) = oneshot::channel::<()>();
889        tokio::spawn(async move {
890            let _first = tokio::net::TcpStream::connect(local).await;
891            let _second = tokio::net::TcpStream::connect(local).await;
892            tokio::time::sleep(Duration::from_millis(200)).await;
893            let _ = cut.send(());
894            std::future::pending::<()>().await;
895        });
896        let mut accepted = 0;
897        let mut ended = Vec::new();
898        let serving = serve(
899            &listener,
900            ServeLimits {
901                max_connections: Some(2),
902            },
903            std::future::pending(),
904            signalled(cut_rx),
905            |stream, _| {
906                accepted += 1;
907                let panics = accepted == 1;
908                async move {
909                    let _held = stream;
910                    assert!(!panics, "the first connection's task panics");
911                    std::future::pending::<Result<(), Error>>().await
912                }
913            },
914            |_, result| ended.push(result.expect_err("neither ends clean").to_string()),
915        );
916        let (stopped, served) = tokio::time::timeout(Duration::from_secs(10), serving)
917            .await
918            .expect("the cut ends the drain");
919        assert_eq!((stopped, served), (StopReason::Limit, 2));
920        assert_eq!(ended.len(), 2, "{ended:?}");
921        let panicked = ended.iter().filter(|end| end.contains("ended abnormally"));
922        let cut = ended.iter().filter(|end| end.contains("was cut"));
923        assert_eq!((panicked.count(), cut.count()), (1, 1), "{ended:?}");
924    }
925}