Skip to main content

base_endpoint/tls/rustls/
rustls_connection.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use super::tls_error;
5use crate::tls::{TlsSession, TlsStream};
6use ::rustls::{Connection, pki_types::CertificateDer};
7use base_scheduler::{
8    Cancellation, OperationError, OperationResult, SocketClose, SocketStreamReader,
9    SocketStreamReading, SocketStreamWriter, SocketStreamWriting,
10};
11use std::io::{self, BufRead, Read, Write};
12
13const BUFFER_SIZE: usize = 16 * 1024;
14
15mod duplex;
16pub use duplex::{EncryptedSocketStreamReader, EncryptedSocketStreamWriter};
17
18/// An established rustls connection owning its scheduler socket and TLS buffers. Sequential operations take the compatible scheduler explicitly; `with_duplex` additionally supervises concurrent plaintext directions. Await operations after cooperative cancellation before closing; dropping an operation provides no asynchronous retirement guarantee.
19///
20/// `write` reports the plaintext bytes accepted into TLS buffering. Follow it with `flush` to send the final buffered data. Cancellation while flushing preserves the exact unsent ciphertext for a later flush; never replay already accepted plaintext. A TLS protocol error is fatal, and a transport error may also make continued use impossible.
21#[derive(Debug)]
22pub struct RustlsConnection<C> {
23    socket: C,
24    state: Connection,
25    incoming: Vec<u8>,
26    incoming_start: usize,
27    incoming_end: usize,
28    outgoing: Vec<u8>,
29    outgoing_start: usize,
30    write_closed: bool,
31}
32
33impl<C> RustlsConnection<C> {
34    pub(super) fn new(socket: C, mut state: Connection) -> Self {
35        state.set_buffer_limit(Some(BUFFER_SIZE));
36        Self {
37            socket,
38            state,
39            incoming: vec![0; BUFFER_SIZE],
40            incoming_start: 0,
41            incoming_end: 0,
42            outgoing: Vec::with_capacity(BUFFER_SIZE),
43            outgoing_start: 0,
44            write_closed: false,
45        }
46    }
47
48    /// Borrow the underlying socket to inspect its addresses. Application data must pass through the TLS connection.
49    pub fn socket(&self) -> &C {
50        &self.socket
51    }
52
53    /// The authenticated peer's certificate chain, if supplied. Server connections only have a peer chain when client authentication completed.
54    pub fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
55        self.state.peer_certificates()
56    }
57
58    pub(super) async fn handshake<S>(
59        &mut self,
60        scheduler: &S,
61        cancellation: &Cancellation,
62    ) -> OperationResult<()>
63    where
64        S: SocketStreamReading<Socket = C> + SocketStreamWriting + ?Sized,
65        C: 'static,
66    {
67        let mut reader = scheduler.socket_stream_reader(&self.socket)?;
68        let mut writer = match scheduler.socket_stream_writer(&self.socket) {
69            Ok(writer) => writer,
70            Err(error) => {
71                let _ = reader.close().await;
72                return Err(error.into());
73            }
74        };
75        let result: OperationResult<()> = async {
76            loop {
77                self.send(&mut writer, cancellation).await?;
78                if !self.state.is_handshaking() {
79                    return Ok(());
80                }
81                if self.receive(&mut reader, cancellation).await? == 0 {
82                    return Err(io::Error::new(
83                        io::ErrorKind::UnexpectedEof,
84                        "peer closed during TLS handshake",
85                    )
86                    .into());
87                }
88            }
89        }
90        .await;
91        self.send_alert_on_error(&result, &mut writer, cancellation)
92            .await;
93        let read_closed = reader.close().await;
94        let write_closed = writer.close().await;
95        result?;
96        read_closed?;
97        write_closed?;
98        Ok(())
99    }
100
101    /// Read decrypted bytes, driving pending TLS control output as needed. A nonempty buffer returns zero only after an authenticated TLS close notification. Transport EOF without that notification reports `UnexpectedEof` once buffered plaintext is consumed. An empty buffer returns zero after checking cancellation.
102    pub async fn read<S>(
103        &mut self,
104        scheduler: &S,
105        buffer: &mut [u8],
106        cancellation: &Cancellation,
107    ) -> OperationResult<usize>
108    where
109        S: SocketStreamReading<Socket = C> + SocketStreamWriting + ?Sized,
110        C: 'static,
111    {
112        cancellation.check()?;
113        if buffer.is_empty() {
114            return Ok(0);
115        }
116        let mut reader = scheduler.socket_stream_reader(&self.socket)?;
117        let mut writer = match scheduler.socket_stream_writer(&self.socket) {
118            Ok(writer) => writer,
119            Err(error) => {
120                let _ = reader.close().await;
121                return Err(error.into());
122            }
123        };
124        let result: OperationResult<()> = async {
125            loop {
126                self.send(&mut writer, cancellation).await?;
127                // Keep plaintext in rustls until both lanes have retired, so a cleanup error cannot hide an already consumed application-data prefix.
128                match self.state.reader().fill_buf() {
129                    Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
130                        self.receive(&mut reader, cancellation).await?;
131                    }
132                    Ok(_) => return Ok(()),
133                    Err(error) => return Err(error.into()),
134                }
135            }
136        }
137        .await;
138        self.send_alert_on_error(&result, &mut writer, cancellation)
139            .await;
140        let read_closed = reader.close().await;
141        let write_closed = writer.close().await;
142        result?;
143        read_closed?;
144        write_closed?;
145        self.state.reader().read(buffer).map_err(Into::into)
146    }
147
148    /// Flush prior output, then accept a possibly short plaintext prefix into TLS buffering. The accepted prefix is reported without any subsequent asynchronous wait. Call `flush` to send it; later writes and reads also drive queued output.
149    pub async fn write<S>(
150        &mut self,
151        scheduler: &S,
152        buffer: &[u8],
153        cancellation: &Cancellation,
154    ) -> OperationResult<usize>
155    where
156        S: SocketStreamWriting<Socket = C> + ?Sized,
157        C: 'static,
158    {
159        cancellation.check()?;
160        if self.write_closed {
161            return Err(
162                io::Error::new(io::ErrorKind::BrokenPipe, "TLS output is shut down").into(),
163            );
164        }
165        self.flush(scheduler, cancellation).await?;
166        cancellation.check()?;
167        self.state.writer().write(buffer).map_err(Into::into)
168    }
169
170    /// Send all queued TLS records, including pending ciphertext from a cancelled flush. Cancellation does not discard or duplicate bytes already accepted by TLS.
171    pub async fn flush<S>(
172        &mut self,
173        scheduler: &S,
174        cancellation: &Cancellation,
175    ) -> OperationResult<()>
176    where
177        S: SocketStreamWriting<Socket = C> + ?Sized,
178        C: 'static,
179    {
180        cancellation.check()?;
181        let mut writer = scheduler.socket_stream_writer(&self.socket)?;
182        let result = self.send(&mut writer, cancellation).await;
183        let closed = writer.close().await;
184        result?;
185        closed?;
186        Ok(())
187    }
188
189    /// Send a TLS close notification and stop accepting plaintext writes. Reads remain available. This does not wait for the peer's close notification or close the socket. Retry after cancellation to finish sending the same notification.
190    pub async fn shutdown<S>(
191        &mut self,
192        scheduler: &S,
193        cancellation: &Cancellation,
194    ) -> OperationResult<()>
195    where
196        S: SocketStreamWriting<Socket = C> + ?Sized,
197        C: 'static,
198    {
199        cancellation.check()?;
200        if !self.write_closed {
201            self.state.send_close_notify();
202            self.write_closed = true;
203        }
204        self.flush(scheduler, cancellation).await
205    }
206
207    /// Attempt TLS shutdown, then consume and close the socket even if shutdown failed or was cancelled. Preserve the shutdown error if socket closure also fails.
208    pub async fn close<S>(
209        mut self,
210        scheduler: &S,
211        cancellation: &Cancellation,
212    ) -> OperationResult<()>
213    where
214        S: SocketStreamWriting<Socket = C> + SocketClose + ?Sized,
215        C: 'static,
216    {
217        let result = self.shutdown(scheduler, cancellation).await;
218        let closed = scheduler.socket_close(self.socket).await;
219        result?;
220        closed?;
221        Ok(())
222    }
223
224    /// Consume and close the socket without sending a TLS close notification. Use after fatal errors or when graceful output is not required.
225    pub async fn abort<S>(self, scheduler: &S) -> io::Result<()>
226    where
227        S: SocketClose<Socket = C> + ?Sized,
228    {
229        scheduler.socket_close(self.socket).await
230    }
231
232    async fn send<W: SocketStreamWriter>(
233        &mut self,
234        writer: &mut W,
235        cancellation: &Cancellation,
236    ) -> OperationResult<()> {
237        loop {
238            if self.outgoing_start == self.outgoing.len() {
239                self.outgoing.clear();
240                self.outgoing_start = 0;
241                if !self.state.wants_write() {
242                    break;
243                }
244                self.outgoing.resize(BUFFER_SIZE, 0);
245                // Rustls reports only destination errors; a memory cursor has none.
246                let count = self
247                    .state
248                    .write_tls(&mut io::Cursor::new(self.outgoing.as_mut_slice()))
249                    .expect("writing TLS records to memory cannot fail");
250                self.outgoing.truncate(count);
251            }
252            let count = writer
253                .write(&self.outgoing[self.outgoing_start..], cancellation)
254                .await?;
255            if count == 0 {
256                return Err(io::Error::new(
257                    io::ErrorKind::WriteZero,
258                    "TLS transport made no write progress",
259                )
260                .into());
261            }
262            self.outgoing_start += count;
263        }
264        writer.flush(cancellation).await
265    }
266
267    // Rustls queues a fatal alert when packet processing fails. Try to deliver it using the caller's cancellation signal, preserving the original TLS error if alert output fails or is cancelled.
268    async fn send_alert_on_error<W: SocketStreamWriter>(
269        &mut self,
270        result: &OperationResult<()>,
271        writer: &mut W,
272        cancellation: &Cancellation,
273    ) {
274        if matches!(result, Err(OperationError::Io(error)) if error.get_ref().is_some_and(|source| source.is::<::rustls::Error>()))
275        {
276            let _ = self.send(writer, cancellation).await;
277        }
278    }
279
280    async fn receive<R: SocketStreamReader>(
281        &mut self,
282        reader: &mut R,
283        cancellation: &Cancellation,
284    ) -> OperationResult<usize> {
285        if self.incoming_start == self.incoming_end {
286            self.incoming_start = 0;
287            self.incoming_end = reader.read_into(&mut self.incoming, cancellation).await?;
288        }
289        let mut input = &self.incoming[self.incoming_start..self.incoming_end];
290        let count = self.state.read_tls(&mut input)?;
291        self.incoming_start += count;
292        self.state.process_new_packets().map_err(tls_error)?;
293        Ok(count)
294    }
295}
296
297impl<C: Send> TlsSession for RustlsConnection<C> {
298    fn negotiated_protocol(&self) -> Option<&[u8]> {
299        self.state.alpn_protocol()
300    }
301}
302
303impl<S> TlsStream<S> for RustlsConnection<S::Socket>
304where
305    S: SocketStreamReading + SocketStreamWriting + SocketClose + ?Sized,
306    S::Socket: 'static,
307{
308    async fn read(
309        &mut self,
310        scheduler: &S,
311        buffer: &mut [u8],
312        cancellation: &Cancellation,
313    ) -> OperationResult<usize> {
314        Self::read(self, scheduler, buffer, cancellation).await
315    }
316
317    async fn write(
318        &mut self,
319        scheduler: &S,
320        buffer: &[u8],
321        cancellation: &Cancellation,
322    ) -> OperationResult<usize> {
323        Self::write(self, scheduler, buffer, cancellation).await
324    }
325
326    async fn flush(&mut self, scheduler: &S, cancellation: &Cancellation) -> OperationResult<()> {
327        Self::flush(self, scheduler, cancellation).await
328    }
329
330    async fn shutdown(
331        &mut self,
332        scheduler: &S,
333        cancellation: &Cancellation,
334    ) -> OperationResult<()> {
335        Self::shutdown(self, scheduler, cancellation).await
336    }
337
338    async fn close(self, scheduler: &S, cancellation: &Cancellation) -> OperationResult<()> {
339        Self::close(self, scheduler, cancellation).await
340    }
341
342    async fn abort(self, scheduler: &S) -> io::Result<()> {
343        Self::abort(self, scheduler).await
344    }
345}
346
347#[cfg(test)]
348mod tests;