Skip to main content

structured_proxy/
serve.rs

1//! Running a [`ProxyService`] on a TCP listener: HTTP/1.1 and HTTP/2 on one
2//! port, optionally behind TLS, with an optional cap on open connections and
3//! a graceful shutdown.
4
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::task::{Context, Poll};
9use std::time::Duration;
10
11use hyper_util::rt::{TokioIo, TokioTimer};
12use hyper_util::server::conn::auto::Builder;
13use hyper_util::server::graceful::GracefulConnection;
14use hyper_util::service::TowerToHyperService;
15use pin_project_lite::pin_project;
16use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
17use tokio::net::TcpListener;
18use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
19use tokio::task::JoinSet;
20use tonic::transport::server::Connected;
21
22use crate::service::{ConnectionInfo, ProxyService};
23use crate::upstream::Upstream;
24
25mod idle;
26
27/// How [`serve_with`] runs its listener.
28///
29/// # Examples
30///
31/// ```
32/// use std::time::Duration;
33/// use structured_proxy::ServeOptions;
34///
35/// let options = ServeOptions::new()
36///     .max_connections(10_000)
37///     .idle_timeout(Some(Duration::from_secs(120)));
38/// # let _ = options;
39/// ```
40#[derive(Clone, Debug)]
41pub struct ServeOptions {
42    max_connections: Option<usize>,
43    tls: Option<Arc<rustls::ServerConfig>>,
44    idle_timeout: Option<Duration>,
45    header_read_timeout: Duration,
46    tls_handshake_timeout: Duration,
47    drain_timeout: Option<Duration>,
48}
49
50impl Default for ServeOptions {
51    fn default() -> Self {
52        Self {
53            max_connections: None,
54            tls: None,
55            idle_timeout: Some(Duration::from_secs(60)),
56            header_read_timeout: Duration::from_secs(30),
57            tls_handshake_timeout: Duration::from_secs(10),
58            // Below the 30 s a Kubernetes pod gets after SIGTERM by default,
59            // so the drain ends before the kill.
60            drain_timeout: Some(Duration::from_secs(25)),
61        }
62    }
63}
64
65impl ServeOptions {
66    /// Cleartext, with no limit on connections; a connection idle for 60 s is
67    /// closed, a client gets 30 s to send the headers of an HTTP/1.1 request
68    /// and 10 s to finish a TLS handshake, and a shutdown waits at most 25 s
69    /// for open connections.
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// How long a shutdown ([`serve_with_shutdown`]) waits for the
75    /// connections still open to finish what they serve; the ones open after
76    /// it are closed. `None` waits for all of them.
77    #[must_use]
78    pub fn drain_timeout(mut self, timeout: Option<Duration>) -> Self {
79        self.drain_timeout = timeout;
80        self
81    }
82
83    /// Close a connection that has had no request in flight for `timeout`
84    /// (gracefully: HTTP/2 gets a GOAWAY); `None` keeps idle connections
85    /// open. A request counts until its response body ends, so a stream keeps
86    /// its connection. Without it an idle client holds a
87    /// [`max_connections`](Self::max_connections) slot for as long as it likes.
88    /// A connection upgraded to another protocol (a WebSocket in a fallback)
89    /// leaves HTTP and this timeout with it; it keeps its slot until it
90    /// closes.
91    #[must_use]
92    pub fn idle_timeout(mut self, timeout: Option<Duration>) -> Self {
93        self.idle_timeout = timeout;
94        self
95    }
96
97    /// How long an HTTP/1.1 client has to send a request's headers, so a
98    /// client that trickles them in holds no connection for long.
99    #[must_use]
100    pub fn header_read_timeout(mut self, timeout: Duration) -> Self {
101        self.header_read_timeout = timeout;
102        self
103    }
104
105    /// How long a client has to finish its TLS handshake before the
106    /// connection is dropped, so a stalled client holds neither a task nor a
107    /// connection slot.
108    #[must_use]
109    pub fn tls_handshake_timeout(mut self, timeout: Duration) -> Self {
110        self.tls_handshake_timeout = timeout;
111        self
112    }
113
114    /// Serve at most `max` connections at once: past it, the next connection
115    /// is accepted when one closes, and waits in the listen backlog until then.
116    ///
117    /// # Panics
118    ///
119    /// `max` is zero, which would never accept a connection.
120    #[must_use]
121    pub fn max_connections(mut self, max: usize) -> Self {
122        assert!(max > 0, "max_connections must be at least 1");
123        self.max_connections = Some(max.min(Semaphore::MAX_PERMITS));
124        self
125    }
126
127    /// Terminate TLS with `config`. Set its `alpn_protocols` to `h2` and
128    /// `http/1.1` for gRPC clients to get HTTP/2; an empty list is filled in
129    /// with those two. A client certificate the config verifies reaches a
130    /// tonic upstream as `Request::peer_certs`.
131    #[must_use]
132    pub fn tls(mut self, mut config: rustls::ServerConfig) -> Self {
133        if config.alpn_protocols.is_empty() {
134            config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
135        }
136        self.tls = Some(Arc::new(config));
137        self
138    }
139}
140
141/// Serve `service` on `listener`: cleartext HTTP/1.1 and HTTP/2 on the same
142/// port, so REST clients and native gRPC clients share it. It runs until the
143/// future is dropped, which closes every connection; [`serve_with_shutdown`]
144/// stops gracefully, and [`serve_with`] adds TLS and a connection limit.
145///
146/// # Errors
147///
148/// None so far: a failed accept is logged and retried, as a full file
149/// descriptor table recovers when connections close.
150///
151/// # Examples
152///
153/// ```no_run
154/// use structured_proxy::ProxyServer;
155///
156/// # async fn run() -> anyhow::Result<()> {
157/// let grpc = tonic::service::Routes::default();
158/// let service = ProxyServer::from_yaml_str("service:\n  name: demo\n")?.service(grpc)?;
159/// let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
160/// structured_proxy::serve(listener, service).await?;
161/// # Ok(())
162/// # }
163/// ```
164pub async fn serve<U: Upstream>(
165    listener: TcpListener,
166    service: ProxyService<U>,
167) -> std::io::Result<()> {
168    serve_with(listener, service, ServeOptions::new()).await
169}
170
171/// [`serve`] with `options`: TLS termination (mTLS when the config verifies
172/// client certificates) and a cap on open connections. Each connection's
173/// service gets its peer, and behind TLS its client certificates, through
174/// [`ProxyService::for_connection`]. The TLS handshake runs in the
175/// connection's own task, so a slow client does not hold up the others.
176///
177/// # Errors
178///
179/// See [`serve`].
180///
181/// # Examples
182///
183/// ```no_run
184/// use structured_proxy::{ProxyServer, ServeOptions};
185///
186/// # async fn run(tls: rustls::ServerConfig) -> anyhow::Result<()> {
187/// let service = ProxyServer::from_yaml_str("service:\n  name: demo\n")?
188///     .service(tonic::service::Routes::default())?;
189/// let listener = tokio::net::TcpListener::bind("0.0.0.0:8443").await?;
190/// let options = ServeOptions::new().tls(tls).max_connections(10_000);
191/// structured_proxy::serve_with(listener, service, options).await?;
192/// # Ok(())
193/// # }
194/// ```
195pub async fn serve_with<U: Upstream>(
196    listener: TcpListener,
197    service: ProxyService<U>,
198    options: ServeOptions,
199) -> std::io::Result<()> {
200    serve_with_shutdown(listener, service, options, std::future::pending()).await
201}
202
203/// [`serve_with`], until `signal` resolves; then a graceful shutdown. The
204/// listening socket closes, so new connections are refused; a connection
205/// still in its TLS handshake or waiting for its `max_connections` slot is
206/// dropped; every open connection is asked to wind down (HTTP/2 gets a GOAWAY,
207/// so its client opens no new streams; HTTP/1.1 closes after the response in
208/// progress), and requests and streams in flight finish. The future resolves
209/// once every connection has closed, or after
210/// [`drain_timeout`](ServeOptions::drain_timeout), closing the ones still
211/// open. Dropping it closes every connection at once.
212///
213/// A connection a fallback upgraded (a WebSocket) belongs to the fallback's
214/// own task once upgraded, and closes when that task lets it go.
215///
216/// # Errors
217///
218/// See [`serve`].
219///
220/// # Examples
221///
222/// ```no_run
223/// use structured_proxy::{ProxyServer, ServeOptions};
224///
225/// # async fn run() -> anyhow::Result<()> {
226/// let service = ProxyServer::from_yaml_str("service:\n  name: demo\n")?
227///     .service(tonic::service::Routes::default())?;
228/// let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
229/// let shutdown = async {
230///     tokio::signal::ctrl_c().await.expect("the signal handler installs");
231/// };
232/// structured_proxy::serve_with_shutdown(listener, service, ServeOptions::new(), shutdown).await?;
233/// # Ok(())
234/// # }
235/// ```
236pub async fn serve_with_shutdown<U, F>(
237    listener: TcpListener,
238    service: ProxyService<U>,
239    options: ServeOptions,
240    signal: F,
241) -> std::io::Result<()>
242where
243    U: Upstream,
244    F: Future<Output = ()>,
245{
246    let slots = options
247        .max_connections
248        .map(|max| Arc::new(Semaphore::new(max)));
249    let acceptor = options.tls.map(tokio_rustls::TlsAcceptor::from);
250    let handshake_timeout = options.tls_handshake_timeout;
251    let limits = ConnectionLimits {
252        idle_timeout: options.idle_timeout,
253        header_read_timeout: options.header_read_timeout,
254    };
255    // Tells every connection to wind down. The connections are this future's
256    // own tasks, so dropping it ends them too.
257    let (stop, stopping) = watch::channel(false);
258    let mut connections = JoinSet::new();
259    let streams = StreamExecutor::default();
260    let _streams_end_with_this_future = streams.cancel_on_drop();
261    tokio::pin!(signal);
262    // Every wait below also reaps finished connections, so the set holds only
263    // open ones even while no client arrives.
264    'serve: loop {
265        // The slot is taken before the accept, so a full server leaves new
266        // connections in the kernel's backlog instead of accepting and
267        // dropping them.
268        let slot = loop {
269            tokio::select! {
270                biased;
271                () = &mut signal => break 'serve,
272                Some(_) = connections.join_next() => {}
273                slot = take_slot(slots.as_ref()) => break slot,
274            }
275        };
276        let tcp = loop {
277            tokio::select! {
278                biased;
279                () = &mut signal => break 'serve,
280                Some(_) = connections.join_next() => {}
281                accepted = listener.accept() => match accepted {
282                    Ok((tcp, _)) => break tcp,
283                    Err(error) => tokio::select! {
284                        biased;
285                        () = &mut signal => break 'serve,
286                        () = accept_failed(error) => {}
287                    },
288                },
289            }
290        };
291        let service = service.clone();
292        let acceptor = acceptor.clone();
293        let stopping = stopping.clone();
294        let streams = streams.clone();
295        connections.spawn(async move {
296            // Small gRPC frames and REST answers are latency-bound.
297            if let Err(error) = tcp.set_nodelay(true) {
298                tracing::debug!(%error, "cannot set TCP_NODELAY");
299            }
300            match acceptor {
301                None => {
302                    let service = service.for_connection(tcp.connect_info());
303                    let io = SlotIo { io: tcp, slot };
304                    serve_connection(io, service, limits, streams, stopping).await;
305                }
306                Some(acceptor) => {
307                    // The slot is held through the handshake, then by the
308                    // stream. A handshake still running at shutdown is not
309                    // finished: the connection would only be closed again.
310                    let handshake = tokio::time::timeout(handshake_timeout, acceptor.accept(tcp));
311                    let stream = tokio::select! {
312                        handshake = handshake => match handshake {
313                            Ok(Ok(stream)) => stream,
314                            Ok(Err(error)) => {
315                                tracing::debug!(%error, "TLS handshake failed");
316                                return;
317                            }
318                            Err(_) => {
319                                tracing::debug!("TLS handshake timed out");
320                                return;
321                            }
322                        },
323                        () = stopped(stopping.clone()) => return,
324                    };
325                    let service =
326                        service.for_connection(ConnectionInfo::tls(stream.connect_info()));
327                    let io = SlotIo { io: stream, slot };
328                    serve_connection(io, service, limits, streams, stopping).await;
329                }
330            }
331        });
332    }
333    // No new connections from here: the listening socket closes, then the
334    // open connections wind down.
335    drop(listener);
336    stop.send_replace(true);
337    streams.close();
338    let drained = async {
339        while connections.join_next().await.is_some() {}
340        streams.finished().await;
341    };
342    match options.drain_timeout {
343        None => drained.await,
344        Some(timeout) => {
345            if tokio::time::timeout(timeout, drained).await.is_err() {
346                tracing::warn!(
347                    open = connections.len(),
348                    "shutdown drain timed out; closing the connections still open"
349                );
350                connections.shutdown().await;
351                streams.stop().await;
352            }
353        }
354    }
355    Ok(())
356}
357
358/// Spawns the stream tasks of HTTP/2 connections (hyper runs each stream's
359/// service in a task of its own) under one tracker, so a shutdown can wait
360/// for them as it does for the connections.
361///
362/// Each task also ends when [`stop`](Self::stop) cancels them: a closed
363/// connection resets its streams, but a stream ending on that reset is
364/// hyper's behaviour, not something a bounded shutdown should rest on.
365#[derive(Clone, Default)]
366struct StreamExecutor {
367    tasks: tokio_util::task::TaskTracker,
368    cancel: tokio_util::sync::CancellationToken,
369}
370
371impl StreamExecutor {
372    /// No new stream after this; the ones running may finish.
373    fn close(&self) {
374        self.tasks.close();
375    }
376
377    /// Resolves once every stream task has finished on its own.
378    async fn finished(&self) {
379        self.tasks.wait().await;
380    }
381
382    /// Ends every stream task still running and waits for them.
383    async fn stop(&self) {
384        self.cancel.cancel();
385        self.tasks.wait().await;
386    }
387
388    /// Cancels every stream task when dropped, so they end with the future
389    /// that owns the server even if it is dropped mid-shutdown.
390    fn cancel_on_drop(&self) -> tokio_util::sync::DropGuard {
391        self.cancel.clone().drop_guard()
392    }
393}
394
395impl<F> hyper::rt::Executor<F> for StreamExecutor
396where
397    F: Future + Send + 'static,
398    F::Output: Send + 'static,
399{
400    fn execute(&self, future: F) {
401        self.tasks
402            .spawn(self.cancel.clone().run_until_cancelled_owned(future));
403    }
404}
405
406/// A `max_connections` slot, or none without a limit.
407async fn take_slot(slots: Option<&Arc<Semaphore>>) -> Option<OwnedSemaphorePermit> {
408    match slots {
409        Some(slots) => Some(
410            Arc::clone(slots)
411                .acquire_owned()
412                .await
413                .expect("the connection semaphore is never closed"),
414        ),
415        None => None,
416    }
417}
418
419/// Resolves once the server shuts down.
420async fn stopped(mut stopping: watch::Receiver<bool>) {
421    // A closed channel means the serve future is gone: stop as well.
422    stopping.wait_for(|stop| *stop).await.ok();
423}
424
425pin_project! {
426    /// A connection's IO holding its `max_connections` slot for as long as
427    /// the socket is open. hyper hands the IO of an upgraded connection (a
428    /// WebSocket in a fallback) to the upgrading service and finishes the
429    /// connection future, so the slot has to live with the IO, not the task.
430    struct SlotIo<I> {
431        #[pin]
432        io: I,
433        slot: Option<OwnedSemaphorePermit>,
434    }
435}
436
437impl<I: AsyncRead> AsyncRead for SlotIo<I> {
438    #[inline]
439    fn poll_read(
440        self: Pin<&mut Self>,
441        cx: &mut Context<'_>,
442        buf: &mut ReadBuf<'_>,
443    ) -> Poll<std::io::Result<()>> {
444        self.project().io.poll_read(cx, buf)
445    }
446}
447
448impl<I: AsyncWrite> AsyncWrite for SlotIo<I> {
449    #[inline]
450    fn poll_write(
451        self: Pin<&mut Self>,
452        cx: &mut Context<'_>,
453        buf: &[u8],
454    ) -> Poll<std::io::Result<usize>> {
455        self.project().io.poll_write(cx, buf)
456    }
457
458    #[inline]
459    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
460        self.project().io.poll_flush(cx)
461    }
462
463    #[inline]
464    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
465        self.project().io.poll_shutdown(cx)
466    }
467
468    #[inline]
469    fn poll_write_vectored(
470        self: Pin<&mut Self>,
471        cx: &mut Context<'_>,
472        bufs: &[std::io::IoSlice<'_>],
473    ) -> Poll<std::io::Result<usize>> {
474        self.project().io.poll_write_vectored(cx, bufs)
475    }
476
477    #[inline]
478    fn is_write_vectored(&self) -> bool {
479        self.io.is_write_vectored()
480    }
481}
482
483/// The timeouts every connection is served under.
484#[derive(Clone, Copy, Debug)]
485struct ConnectionLimits {
486    idle_timeout: Option<Duration>,
487    header_read_timeout: Duration,
488}
489
490/// HTTP/1.1 or HTTP/2, whichever the client speaks, on one connection, until
491/// it closes, idles out or the server shuts down.
492async fn serve_connection<U, I>(
493    io: I,
494    service: ProxyService<U>,
495    limits: ConnectionLimits,
496    streams: StreamExecutor,
497    stopping: watch::Receiver<bool>,
498) where
499    U: Upstream,
500    I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
501{
502    let mut builder = Builder::new(streams);
503    // hyper times nothing without a timer: its own default header read
504    // timeout is dropped with a warning.
505    builder
506        .http1()
507        .timer(TokioTimer::new())
508        .header_read_timeout(limits.header_read_timeout);
509    builder.http2().timer(TokioTimer::new());
510    let served = match limits.idle_timeout {
511        None => {
512            let connection = builder.serve_connection_with_upgrades(
513                TokioIo::new(io),
514                TowerToHyperService::new(service),
515            );
516            drive(connection, std::future::pending(), stopping).await
517        }
518        Some(timeout) => {
519            let activity = Arc::new(idle::Activity::default());
520            let service = idle::Tracked {
521                inner: service,
522                activity: Arc::clone(&activity),
523            };
524            let connection = builder.serve_connection_with_upgrades(
525                TokioIo::new(io),
526                TowerToHyperService::new(service),
527            );
528            drive(connection, activity.idle_for(timeout), stopping).await
529        }
530    };
531    if let Err(error) = served {
532        tracing::debug!(%error, "connection ended");
533    }
534}
535
536/// Serve `connection` until it ends; when `idle` resolves or the server
537/// shuts down first, it is asked to wind down (HTTP/2 gets a GOAWAY, HTTP/1.1
538/// closes after the request in progress) and served until it has.
539async fn drive<C: GracefulConnection>(
540    connection: C,
541    idle: impl Future<Output = ()>,
542    stopping: watch::Receiver<bool>,
543) -> Result<(), C::Error> {
544    tokio::pin!(connection);
545    tokio::select! {
546        served = connection.as_mut() => served,
547        () = idle => {
548            connection.as_mut().graceful_shutdown();
549            connection.await
550        }
551        () = stopped(stopping) => {
552            connection.as_mut().graceful_shutdown();
553            connection.await
554        }
555    }
556}
557
558/// A failed accept: a connection the client gave up on is skipped; anything
559/// else (a full file descriptor table) is logged and retried a second later,
560/// the pause that lets open connections close.
561async fn accept_failed(error: std::io::Error) {
562    use std::io::ErrorKind;
563    if matches!(
564        error.kind(),
565        ErrorKind::ConnectionRefused | ErrorKind::ConnectionAborted | ErrorKind::ConnectionReset
566    ) {
567        return;
568    }
569    tracing::error!(%error, "accepting a connection failed; retrying in 1s");
570    tokio::time::sleep(Duration::from_secs(1)).await;
571}
572
573#[cfg(test)]
574mod tests;