Skip to main content

pg_proto/
net.rs

1//! TCP establishment, socket configuration, and negotiated network streams.
2
3use std::{
4    fmt, io,
5    pin::Pin,
6    task::{Context, Poll},
7    time::Duration,
8};
9
10use socket2::{SockRef, TcpKeepalive};
11use tokio::{
12    io::{AsyncRead, AsyncWrite, ReadBuf},
13    net::TcpStream,
14    time::sleep,
15};
16
17use crate::{
18    auth::TlsServerEndPoint,
19    tls::{ClientTls, ServerTls},
20};
21
22/// A PostgreSQL network transport after optional TLS negotiation.
23#[derive(Debug)]
24pub enum NetworkStream<S> {
25    /// An unencrypted transport.
26    Plain(S),
27    /// TLS initiated by this endpoint as a client.
28    ClientTls(ClientTls<S>),
29    /// TLS accepted by this endpoint as a server.
30    ServerTls(ServerTls<S>),
31}
32
33impl<S> NetworkStream<S> {
34    /// Wraps an unencrypted transport.
35    pub const fn plain(stream: S) -> Self {
36        Self::Plain(stream)
37    }
38
39    /// Wraps a completed client-side TLS upgrade.
40    pub const fn client_tls(stream: ClientTls<S>) -> Self {
41        Self::ClientTls(stream)
42    }
43
44    /// Wraps a completed server-side TLS upgrade.
45    pub const fn server_tls(stream: ServerTls<S>) -> Self {
46        Self::ServerTls(stream)
47    }
48
49    /// Reports whether the transport is encrypted.
50    pub const fn is_tls(&self) -> bool {
51        !matches!(self, Self::Plain(_))
52    }
53
54    /// Reports whether the transport is unencrypted.
55    pub const fn is_plain(&self) -> bool {
56        matches!(self, Self::Plain(_))
57    }
58
59    /// Returns the plain transport when TLS has not already been negotiated.
60    ///
61    /// # Errors
62    ///
63    /// Returns [`AlreadyTls`] for either negotiated TLS variant.
64    pub fn into_plain(self) -> Result<S, AlreadyTls> {
65        match self {
66            Self::Plain(stream) => Ok(stream),
67            Self::ClientTls(_) | Self::ServerTls(_) => Err(AlreadyTls),
68        }
69    }
70
71    /// Returns RFC 5929 `tls-server-end-point` bytes for an encrypted stream.
72    pub fn tls_server_end_point(&self) -> Option<&[u8]> {
73        match self {
74            Self::Plain(_) => None,
75            Self::ClientTls(stream) => Some(stream.tls_server_end_point()),
76            Self::ServerTls(stream) => Some(stream.tls_server_end_point()),
77        }
78    }
79}
80
81impl<S> TlsServerEndPoint for NetworkStream<S> {
82    fn tls_server_end_point(&self) -> &[u8] {
83        match self {
84            Self::Plain(_) => &[],
85            Self::ClientTls(stream) => stream.tls_server_end_point(),
86            Self::ServerTls(stream) => stream.tls_server_end_point(),
87        }
88    }
89}
90
91impl<S: AsyncRead + AsyncWrite + Unpin> NetworkStream<S> {
92    /// Splits the negotiated transport into independently owned read and write halves.
93    pub fn split(self) -> (tokio::io::ReadHalf<Self>, tokio::io::WriteHalf<Self>) {
94        tokio::io::split(self)
95    }
96}
97
98/// A plain transport was requested after TLS had already been negotiated.
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub struct AlreadyTls;
101
102impl fmt::Display for AlreadyTls {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        formatter.write_str("transport is already using TLS")
105    }
106}
107
108impl std::error::Error for AlreadyTls {}
109
110impl<S: AsyncRead + AsyncWrite + Unpin> AsyncRead for NetworkStream<S> {
111    fn poll_read(
112        mut self: Pin<&mut Self>,
113        context: &mut Context<'_>,
114        buffer: &mut ReadBuf<'_>,
115    ) -> Poll<io::Result<()>> {
116        match &mut *self {
117            Self::Plain(stream) => Pin::new(stream).poll_read(context, buffer),
118            Self::ClientTls(stream) => Pin::new(stream).poll_read(context, buffer),
119            Self::ServerTls(stream) => Pin::new(stream).poll_read(context, buffer),
120        }
121    }
122}
123
124impl<S: AsyncRead + AsyncWrite + Unpin> AsyncWrite for NetworkStream<S> {
125    fn poll_write(
126        mut self: Pin<&mut Self>,
127        context: &mut Context<'_>,
128        buffer: &[u8],
129    ) -> Poll<io::Result<usize>> {
130        match &mut *self {
131            Self::Plain(stream) => Pin::new(stream).poll_write(context, buffer),
132            Self::ClientTls(stream) => Pin::new(stream).poll_write(context, buffer),
133            Self::ServerTls(stream) => Pin::new(stream).poll_write(context, buffer),
134        }
135    }
136
137    fn poll_flush(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
138        match &mut *self {
139            Self::Plain(stream) => Pin::new(stream).poll_flush(context),
140            Self::ClientTls(stream) => Pin::new(stream).poll_flush(context),
141            Self::ServerTls(stream) => Pin::new(stream).poll_flush(context),
142        }
143    }
144
145    fn poll_shutdown(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<io::Result<()>> {
146        match &mut *self {
147            Self::Plain(stream) => Pin::new(stream).poll_shutdown(context),
148            Self::ClientTls(stream) => Pin::new(stream).poll_shutdown(context),
149            Self::ServerTls(stream) => Pin::new(stream).poll_shutdown(context),
150        }
151    }
152}
153
154/// Retry policy for establishing an outbound TCP connection.
155#[derive(Clone, Copy, Debug, Eq, PartialEq)]
156pub struct ConnectRetry {
157    /// Number of retries after the initial attempt.
158    pub max_retries: u32,
159    /// Initial exponential-backoff delay.
160    pub initial_delay: Duration,
161    /// Upper bound for an individual delay.
162    pub max_delay: Duration,
163}
164
165impl Default for ConnectRetry {
166    fn default() -> Self {
167        Self {
168            max_retries: 3,
169            initial_delay: Duration::from_millis(100),
170            max_delay: Duration::from_secs(2),
171        }
172    }
173}
174
175/// Connects to `address`, retrying failures with capped exponential backoff.
176///
177/// # Errors
178///
179/// Returns the final connection error after the configured attempts are exhausted.
180pub async fn connect_with_retry(address: &str, retry: ConnectRetry) -> io::Result<TcpStream> {
181    let mut retries = 0_u32;
182    loop {
183        match TcpStream::connect(address).await {
184            Ok(stream) => return Ok(stream),
185            Err(error) if retries == retry.max_retries => return Err(error),
186            Err(_) => {
187                let exponent = retries.min(63);
188                let multiplier = 1_u64 << exponent;
189                let delay = retry
190                    .initial_delay
191                    .saturating_mul(u32::try_from(multiplier).unwrap_or(u32::MAX))
192                    .min(retry.max_delay);
193                sleep(delay).await;
194                retries = retries.saturating_add(1);
195            }
196        }
197    }
198}
199
200/// Best-effort TCP socket configuration.
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub struct TcpSettings {
203    /// Whether to disable Nagle's algorithm.
204    pub no_delay: bool,
205    /// Optional TCP user timeout on supported operating systems.
206    pub user_timeout: Option<Duration>,
207    /// Optional keepalive idle time.
208    pub keepalive_time: Option<Duration>,
209    /// Optional keepalive probe interval.
210    pub keepalive_interval: Option<Duration>,
211    /// Optional number of failed probes before the connection is closed.
212    pub keepalive_retries: Option<u32>,
213}
214
215impl Default for TcpSettings {
216    fn default() -> Self {
217        Self {
218            no_delay: true,
219            user_timeout: None,
220            keepalive_time: None,
221            keepalive_interval: None,
222            keepalive_retries: None,
223        }
224    }
225}
226
227/// One socket option which could not be applied.
228#[derive(Debug)]
229pub struct TcpConfigurationError {
230    /// Stable socket-option name.
231    pub option: &'static str,
232    /// Operating-system error returned while applying the option.
233    pub source: io::Error,
234}
235
236impl fmt::Display for TcpConfigurationError {
237    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(
239            formatter,
240            "failed to configure {}: {}",
241            self.option, self.source
242        )
243    }
244}
245
246impl std::error::Error for TcpConfigurationError {
247    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
248        Some(&self.source)
249    }
250}
251
252/// Applies every configured TCP option and reports individual failures.
253#[must_use]
254pub fn configure_tcp(stream: &TcpStream, settings: TcpSettings) -> Vec<TcpConfigurationError> {
255    let mut errors = Vec::new();
256    if let Err(source) = stream.set_nodelay(settings.no_delay) {
257        errors.push(TcpConfigurationError {
258            option: "TCP_NODELAY",
259            source,
260        });
261    }
262
263    let socket = SockRef::from(stream);
264    #[cfg(target_os = "linux")]
265    if let Some(timeout) = settings.user_timeout
266        && let Err(source) = socket.set_tcp_user_timeout(Some(timeout))
267    {
268        errors.push(TcpConfigurationError {
269            option: "TCP_USER_TIMEOUT",
270            source,
271        });
272    }
273
274    if settings.keepalive_time.is_some()
275        || settings.keepalive_interval.is_some()
276        || settings.keepalive_retries.is_some()
277    {
278        if let Err(source) = socket.set_keepalive(true) {
279            errors.push(TcpConfigurationError {
280                option: "SO_KEEPALIVE",
281                source,
282            });
283            return errors;
284        }
285        let mut keepalive = TcpKeepalive::new();
286        if let Some(time) = settings.keepalive_time {
287            keepalive = keepalive.with_time(time);
288        }
289        if let Some(interval) = settings.keepalive_interval {
290            keepalive = keepalive.with_interval(interval);
291        }
292        if let Some(retries) = settings.keepalive_retries {
293            keepalive = keepalive.with_retries(retries);
294        }
295        if let Err(source) = socket.set_tcp_keepalive(&keepalive) {
296            errors.push(TcpConfigurationError {
297                option: "TCP_KEEPALIVE",
298                source,
299            });
300        }
301    }
302    errors
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308    use tokio::net::TcpListener;
309
310    #[tokio::test]
311    async fn network_stream_delegates_plain_io() {
312        let (client, server) = tokio::io::duplex(16);
313        let mut client = NetworkStream::plain(client);
314        let mut server = NetworkStream::plain(server);
315        tokio::io::AsyncWriteExt::write_all(&mut client, b"ping")
316            .await
317            .unwrap();
318        let mut bytes = [0; 4];
319        tokio::io::AsyncReadExt::read_exact(&mut server, &mut bytes)
320            .await
321            .unwrap();
322        assert_eq!(&bytes, b"ping");
323        assert!(client.is_plain());
324        assert_eq!(client.tls_server_end_point(), None);
325    }
326
327    #[tokio::test]
328    async fn connects_with_configurable_retry() {
329        let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
330        let address = listener.local_addr().unwrap().to_string();
331        let client = connect_with_retry(&address, ConnectRetry::default())
332            .await
333            .unwrap();
334        let (_server, _) = listener.accept().await.unwrap();
335        assert!(configure_tcp(&client, TcpSettings::default()).is_empty());
336    }
337}