Skip to main content

dynamic_config_server/
serve.rs

1//! Serving the router over TLS.
2//!
3//! # Why this is fifty lines here rather than a dependency
4//!
5//! `axum-server` is the usual shape and was rejected. The accept loop is the
6//! part of a TLS server where the decisions that matter are made, and all of
7//! them are this crate's to make:
8//!
9//! - **A failed handshake must cost one connection, never the listener.**
10//!   With client certificates required, failed handshakes are *normal
11//!   traffic* — every port scanner and every health checker that does not
12//!   know about the certificate produces one.
13//! - **A connection must not be able to hold a slot forever.** A client that
14//!   opens a socket and sends nothing is free for the client and not free
15//!   for the server, so the handshake has a deadline
16//!   ([`HANDSHAKE_TIMEOUT`]) — and so do the request headers after it
17//!   ([`HEADER_TIMEOUT`]), because a client that completes a handshake and
18//!   then goes quiet costs exactly as much as one that never handshook.
19//! - **Shutdown must end.** One endpoint here answers with a body that
20//!   never ends, so a drain that waits for every response body waits for
21//!   every subscriber to leave; [`DRAIN_TIMEOUT`](crate::DRAIN_TIMEOUT) is what keeps a rollout
22//!   from hanging on its own change stream.
23//! - **A refused handshake must be recorded, and recorded saying nothing.**
24//!   It goes through the same [`AuditSink`](crate::AuditSink) as everything
25//!   else, as `endpoint=tls outcome=unauthenticated`, with no caller, no
26//!   subject and no reason. An operator who has misconfigured a client CA
27//!   needs to see *that* handshakes are failing; nobody needs to see whose.
28//!
29//! `axum-server` would own every one of them and expose none, and its
30//! `tls-rustls` feature selects the `aws-lc-rs` provider — a vendored copy
31//! of AWS-LC, built with cmake — where this crate wants `ring`, which is
32//! already in the workspace's graph. What is used instead is `tokio-rustls`
33//! for the handshake and hyper's own HTTP/1 connection for what follows:
34//! the same two pieces `axum::serve` uses, with the acceptor spliced in
35//! between.
36//!
37//! The rustls configuration itself is *not* built here: it comes from
38//! [`Tls`](crate::tls::Tls), where the key loading, the permission refusal
39//! and the client verifier live.
40
41use std::future::Future;
42use std::io;
43use std::sync::Arc;
44use std::time::Duration;
45
46use axum::Router;
47use hyper::server::conn::http1;
48use hyper_util::rt::{TokioIo, TokioTimer};
49use hyper_util::service::TowerToHyperService;
50use tokio::net::{TcpListener, TcpStream};
51use tokio::sync::watch;
52use tokio::task::JoinSet;
53use tokio_rustls::TlsAcceptor;
54
55use crate::audit::{AuditEntry, AuditSink, Outcome};
56use crate::server::Server;
57
58/// How long a client has to complete a TLS handshake.
59///
60/// Generous for a handshake and short for a socket somebody is squatting on.
61/// It is not a configuration key: a deployment that needs a different number
62/// has an idle timeout of its own in front, and this one exists so that a
63/// server with no such thing in front is still not held open by a client
64/// that connects and says nothing.
65pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
66
67/// How long a connection has, after the handshake, to send request headers.
68///
69/// The handshake deadline covers TLS and stops there. Without this one, a
70/// client that completes a handshake and then sends no request bytes — or
71/// drips an incomplete header a byte at a time — holds a socket and a task
72/// for as long as it likes, which is the exhaustion
73/// [`HANDSHAKE_TIMEOUT`] exists to prevent, moved one step further in.
74///
75/// It bounds *headers*, not the request as a whole: a stream's response body
76/// is meant to run for days, and this deadline stops before the first byte
77/// of one is written.
78pub const HEADER_TIMEOUT: Duration = Duration::from_secs(15);
79
80/// How long the accept loop waits after an accept error before trying again.
81///
82/// An `accept` that fails because the process is out of file descriptors
83/// fails again immediately, and a loop that retries instantly turns a
84/// resource limit into a spin. Ten milliseconds is invisible to a client and
85/// is the difference between busy-waiting and waiting.
86const ACCEPT_BACKOFF: Duration = Duration::from_millis(10);
87
88/// Serves `router` over TLS until `shutdown` completes.
89///
90/// The TLS material and the audit sink both come from `server`, which is the
91/// same one the router was built over; `router` is passed separately so that
92/// an embedder can wrap it — a layer of its own, a different fallback — the
93/// way it can with [`axum::serve()`].
94///
95/// Shutdown is graceful in both halves: the listener stops accepting, and
96/// every connection already open is told to finish the request it is serving
97/// and close. A config server is restarted by a rollout, and dropping the
98/// fetch a pod is making at that moment would make a rollout look like a
99/// configuration failure to whoever is starting up beside it.
100///
101/// Graceful, and bounded: a connection that has not finished within
102/// [`DRAIN_TIMEOUT`](crate::DRAIN_TIMEOUT) is dropped. An open change stream has no end of its
103/// own, so an unbounded drain would be a rollout that waits for its
104/// subscribers rather than the other way round.
105///
106/// # Errors
107///
108/// Only what the listener itself reports. A connection that fails — a
109/// handshake that was refused, a client that went away — ends that
110/// connection and nothing else.
111///
112/// # Panics
113///
114/// If `server` is not configured for TLS. Callers reach this function
115/// through [`Server::tls`], which is `None` exactly when that is so.
116pub async fn serve_tls<S>(
117    listener: TcpListener,
118    router: Router,
119    server: &Server,
120    shutdown: S,
121) -> io::Result<()>
122where
123    // No `'static`: the shutdown future is awaited here rather than spawned,
124    // so it may borrow whatever the caller has to hand — a `CancellationToken`
125    // it owns, a channel on its own stack.
126    S: Future<Output = ()> + Send,
127{
128    let tls = server
129        .tls()
130        .expect("serve_tls is reached through Server::tls, which is None without a `[server.tls]`");
131    let acceptor = TlsAcceptor::from(tls.server_config());
132    let audit = server.audit_sink();
133
134    // `false` until the shutdown future completes, at which point every
135    // connection task sees it and finishes what it is doing.
136    let (closing, closed) = watch::channel(false);
137    let mut connections = JoinSet::new();
138
139    tokio::pin!(shutdown);
140
141    loop {
142        tokio::select! {
143            () = &mut shutdown => break,
144            accepted = listener.accept() => {
145                match accepted {
146                    Ok((stream, _peer)) => {
147                        connections.spawn(connection(
148                            acceptor.clone(),
149                            stream,
150                            router.clone(),
151                            Arc::clone(&audit),
152                            closed.clone(),
153                        ));
154                    }
155                    // Per-connection, not per-server: a client that went away
156                    // between the SYN and the accept, or a descriptor limit.
157                    Err(_) => tokio::time::sleep(ACCEPT_BACKOFF).await,
158                }
159            }
160            // Reaping is part of the loop rather than a task of its own, so
161            // that a long-lived server does not accumulate a `JoinSet` entry
162            // per connection it has ever served.
163            Some(_) = connections.join_next(), if !connections.is_empty() => {}
164        }
165    }
166
167    // The listener first: a client connecting during shutdown gets a refused
168    // connection, which it will retry, rather than a connection that is
169    // accepted and then closed unanswered.
170    drop(listener);
171    let _ = closing.send(true);
172
173    while connections.join_next().await.is_some() {}
174
175    Ok(())
176}
177
178/// One connection: handshake, then HTTP until it ends or shutdown does.
179async fn connection(
180    acceptor: TlsAcceptor,
181    stream: TcpStream,
182    router: Router,
183    audit: Arc<dyn AuditSink>,
184    mut closed: watch::Receiver<bool>,
185) {
186    let handshake = tokio::time::timeout(HANDSHAKE_TIMEOUT, acceptor.accept(stream)).await;
187
188    let stream = match handshake {
189        Ok(Ok(stream)) => stream,
190        // Both failures are recorded identically and say nothing about the
191        // certificate that was or was not presented. A refused handshake is
192        // a caller that presented no usable credential, which is an outcome
193        // this log already has a word for.
194        Ok(Err(_)) | Err(_) => {
195            audit.record(&AuditEntry {
196                caller: None,
197                application: None,
198                profile: None,
199                endpoint: "tls",
200                outcome: Outcome::Unauthenticated,
201                generation: None,
202            });
203
204            return;
205        }
206    };
207
208    let service = TowerToHyperService::new(router);
209
210    let mut builder = http1::Builder::new();
211
212    // The timer is not a detail: hyper reads deadlines from the one the
213    // builder was given, and a `header_read_timeout` set without one is
214    // silently not enforced.
215    builder
216        .timer(TokioTimer::new())
217        .header_read_timeout(HEADER_TIMEOUT);
218
219    let connection = builder.serve_connection(TokioIo::new(stream), service);
220
221    tokio::pin!(connection);
222
223    tokio::select! {
224        _ = connection.as_mut() => {}
225        _ = closed.changed() => {
226            // Finish the request in flight, refuse to start another, close
227            // — but not for longer than `DRAIN_TIMEOUT`, because a stream's
228            // body has no end of its own and would otherwise hold the
229            // rollout open until its client happened to leave.
230            connection.as_mut().graceful_shutdown();
231            let _ = tokio::time::timeout(crate::DRAIN_TIMEOUT, connection).await;
232        }
233    }
234}