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;