Skip to main content

loonfs_server/http/
tls.rs

1//! TLS termination: the certificate/key load and the [`axum::serve`]
2//! listener that wraps accepted TCP connections in a rustls session.
3
4use crate::config::TlsServerConfig;
5use rustls::pki_types::{CertificateDer, PrivateKeyDer};
6use std::future::Future;
7use std::io;
8use std::net::SocketAddr;
9use std::path::Path;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{ready, Context, Poll};
13use std::time::Duration;
14use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
15use tokio::net::{TcpListener, TcpStream};
16use tokio_rustls::server::TlsStream as RustlsStream;
17use tokio_rustls::{Accept, TlsAcceptor};
18
19/// Why the configured TLS identity could not be used. Every variant names
20/// the file it came from; none of them carry key material.
21#[derive(Debug, thiserror::Error)]
22pub enum TlsConfigError {
23    #[error("failed to read `{path}`: {source}")]
24    Read {
25        path: String,
26        #[source]
27        source: io::Error,
28    },
29    #[error("`{path}` is not valid PEM: {reason}")]
30    Pem { path: String, reason: String },
31    #[error(
32        "`{path}` and the certificate in `{cert_path}` do not form a usable identity: {reason}"
33    )]
34    Identity {
35        path: String,
36        cert_path: String,
37        reason: String,
38    },
39}
40
41/// Builds the rustls server configuration from the configured files.
42///
43/// The provider is named rather than taken from the process-wide default:
44/// this process links exactly one, and resolving it here keeps a startup
45/// misconfiguration a returned error instead of a panic deep in rustls.
46pub(super) fn server_config(
47    config: &TlsServerConfig,
48) -> Result<rustls::ServerConfig, TlsConfigError> {
49    let certs = load_cert_chain(&config.cert_path)?;
50    let key = load_private_key(&config.key_path)?;
51    let provider = Arc::new(rustls::crypto::ring::default_provider());
52    let mut server_config = rustls::ServerConfig::builder_with_provider(provider)
53        .with_safe_default_protocol_versions()
54        .map_err(|error| TlsConfigError::Identity {
55            path: config.key_path.clone(),
56            cert_path: config.cert_path.clone(),
57            reason: error.to_string(),
58        })?
59        .with_no_client_auth()
60        .with_single_cert(certs, key)
61        .map_err(|error| TlsConfigError::Identity {
62            path: config.key_path.clone(),
63            cert_path: config.cert_path.clone(),
64            reason: error.to_string(),
65        })?;
66    // Offered in preference order. HTTP/2 first because axum serves it, and
67    // `http/1.1` retained because a client that cannot speak h2 must still
68    // reach the same routes.
69    server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
70    Ok(server_config)
71}
72
73fn load_cert_chain(path: &str) -> Result<Vec<CertificateDer<'static>>, TlsConfigError> {
74    let mut reader = io::BufReader::new(open(path)?);
75    let certs = rustls_pemfile::certs(&mut reader)
76        .collect::<Result<Vec<_>, _>>()
77        .map_err(|error| TlsConfigError::Pem {
78            path: path.to_owned(),
79            reason: error.to_string(),
80        })?;
81    if certs.is_empty() {
82        return Err(TlsConfigError::Pem {
83            path: path.to_owned(),
84            reason: "no CERTIFICATE section found; expected the PEM chain, leaf first".to_owned(),
85        });
86    }
87    Ok(certs)
88}
89
90fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, TlsConfigError> {
91    let mut reader = io::BufReader::new(open(path)?);
92    rustls_pemfile::private_key(&mut reader)
93        .map_err(|error| TlsConfigError::Pem {
94            path: path.to_owned(),
95            reason: error.to_string(),
96        })?
97        .ok_or_else(|| TlsConfigError::Pem {
98            path: path.to_owned(),
99            reason: "no PRIVATE KEY section found; expected a PKCS#8, RSA, or EC private key"
100                .to_owned(),
101        })
102}
103
104fn open(path: &str) -> Result<std::fs::File, TlsConfigError> {
105    std::fs::File::open(Path::new(path)).map_err(|source| TlsConfigError::Read {
106        path: path.to_owned(),
107        source,
108    })
109}
110
111/// A TCP listener that hands [`axum::serve`] TLS connections.
112///
113/// `accept` deliberately returns as soon as the TCP connection is accepted,
114/// before the handshake runs. axum awaits `accept` in the loop that also
115/// dispatches connections, so a handshake performed here would be a
116/// head-of-line block: one client that connects and then stalls would keep
117/// every other client waiting. The handshake instead runs inside the
118/// connection's own task, on the first poll of the returned [`TlsIo`].
119pub(super) struct TlsListener {
120    tcp: TcpListener,
121    acceptor: TlsAcceptor,
122}
123
124impl TlsListener {
125    pub(super) fn new(tcp: TcpListener, config: rustls::ServerConfig) -> Self {
126        Self {
127            tcp,
128            acceptor: TlsAcceptor::from(Arc::new(config)),
129        }
130    }
131}
132
133impl axum::serve::Listener for TlsListener {
134    type Io = TlsIo;
135    type Addr = SocketAddr;
136
137    async fn accept(&mut self) -> (Self::Io, Self::Addr) {
138        loop {
139            match self.tcp.accept().await {
140                Ok((stream, addr)) => {
141                    return (
142                        TlsIo::Handshaking(Box::new(self.acceptor.accept(stream))),
143                        addr,
144                    )
145                }
146                Err(error) => handle_accept_error(error).await,
147            }
148        }
149    }
150
151    fn local_addr(&self) -> io::Result<Self::Addr> {
152        self.tcp.local_addr()
153    }
154}
155
156/// Mirrors what axum's own `Listener for TcpListener` does with a failed
157/// accept, because the trait makes each implementor responsible for it:
158/// a per-connection error is dropped, and anything else — `EMFILE` being the
159/// case worth naming — is logged and retried after a pause rather than
160/// ending the accept loop.
161async fn handle_accept_error(error: io::Error) {
162    if matches!(
163        error.kind(),
164        io::ErrorKind::ConnectionRefused
165            | io::ErrorKind::ConnectionAborted
166            | io::ErrorKind::ConnectionReset
167    ) {
168        return;
169    }
170    tracing::error!("accept error: {error}");
171    accept_retry_pause().await;
172}
173
174#[allow(clippy::disallowed_methods)]
175// Backs off an accept loop that would otherwise spin on a resource the
176// process has run out of. No protocol time depends on it.
177async fn accept_retry_pause() {
178    tokio::time::sleep(ACCEPT_RETRY_PAUSE).await;
179}
180
181const ACCEPT_RETRY_PAUSE: Duration = Duration::from_secs(1);
182
183/// One accepted connection, before or after its TLS handshake.
184///
185/// A handshake failure is this connection's failure and nothing else's: it
186/// surfaces as an `io::Error` to the task serving this socket, which drops
187/// it. The listener keeps accepting, so a plaintext client that reaches the
188/// TLS port loses its own connection and no other.
189pub(super) enum TlsIo {
190    Handshaking(Box<Accept<TcpStream>>),
191    Ready(Box<RustlsStream<TcpStream>>),
192    /// The handshake failed. Held so a later poll reports the failure again
193    /// instead of polling a future that has already completed.
194    Failed,
195}
196
197impl TlsIo {
198    /// Drives the handshake to completion, then yields the negotiated
199    /// stream. Every read and write goes through here, so the handshake
200    /// happens exactly once, on whichever comes first.
201    fn poll_stream(
202        &mut self,
203        cx: &mut Context<'_>,
204    ) -> Poll<io::Result<Pin<&mut RustlsStream<TcpStream>>>> {
205        if let Self::Handshaking(accept) = self {
206            match Pin::new(accept.as_mut()).poll(cx) {
207                Poll::Ready(Ok(stream)) => *self = Self::Ready(Box::new(stream)),
208                Poll::Ready(Err(error)) => {
209                    *self = Self::Failed;
210                    return Poll::Ready(Err(error));
211                }
212                Poll::Pending => return Poll::Pending,
213            }
214        }
215        match self {
216            Self::Ready(stream) => Poll::Ready(Ok(Pin::new(stream.as_mut()))),
217            // `Handshaking` cannot reach here: the block above either
218            // replaced it or returned.
219            Self::Handshaking(_) | Self::Failed => Poll::Ready(Err(handshake_failed())),
220        }
221    }
222}
223
224fn handshake_failed() -> io::Error {
225    io::Error::new(
226        io::ErrorKind::InvalidData,
227        "tls handshake failed on this connection",
228    )
229}
230
231impl AsyncRead for TlsIo {
232    fn poll_read(
233        self: Pin<&mut Self>,
234        cx: &mut Context<'_>,
235        buf: &mut ReadBuf<'_>,
236    ) -> Poll<io::Result<()>> {
237        ready!(self.get_mut().poll_stream(cx))?.poll_read(cx, buf)
238    }
239}
240
241impl AsyncWrite for TlsIo {
242    fn poll_write(
243        self: Pin<&mut Self>,
244        cx: &mut Context<'_>,
245        buf: &[u8],
246    ) -> Poll<io::Result<usize>> {
247        ready!(self.get_mut().poll_stream(cx))?.poll_write(cx, buf)
248    }
249
250    fn poll_write_vectored(
251        self: Pin<&mut Self>,
252        cx: &mut Context<'_>,
253        bufs: &[io::IoSlice<'_>],
254    ) -> Poll<io::Result<usize>> {
255        ready!(self.get_mut().poll_stream(cx))?.poll_write_vectored(cx, bufs)
256    }
257
258    /// Constant for the wrapped stream in every state, so reporting it
259    /// before the handshake finishes cannot contradict what the negotiated
260    /// stream then does.
261    fn is_write_vectored(&self) -> bool {
262        true
263    }
264
265    fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
266        ready!(self.get_mut().poll_stream(cx))?.poll_flush(cx)
267    }
268
269    /// A connection whose handshake never finished has no session to close,
270    /// and completing one on the way out would make shutdown wait on a peer
271    /// that has already stopped mattering. Dropping the socket is the whole
272    /// close in that state.
273    fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
274        match self.get_mut() {
275            TlsIo::Ready(stream) => Pin::new(stream.as_mut()).poll_shutdown(cx),
276            TlsIo::Handshaking(_) | TlsIo::Failed => Poll::Ready(Ok(())),
277        }
278    }
279}