use std::net::SocketAddr;
use std::time::Duration;
use tokio::io::{BufReader, BufWriter, ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use crate::error::{Error, Result};
use crate::tcp::handshake::{HandshakeConfig, handshake};
use crate::tcp::protocol::ServerHello;
use crate::tcp::transport::{CONN_READ_BUFFER, CONN_WRITE_BUFFER, MaybeTlsStream};
const TCP_KEEPALIVE_IDLE: Duration = Duration::from_secs(60);
const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(20);
const TCP_KEEPALIVE_RETRIES: u32 = 3;
async fn connect_socket(addr: SocketAddr) -> Result<TcpStream> {
let socket = TcpStream::connect(addr).await.map_err(Error::from)?;
socket.set_nodelay(true).map_err(Error::from)?;
let sock_ref = socket2::SockRef::from(&socket);
let keepalive = socket2::TcpKeepalive::new()
.with_time(TCP_KEEPALIVE_IDLE)
.with_interval(TCP_KEEPALIVE_INTERVAL)
.with_retries(TCP_KEEPALIVE_RETRIES);
sock_ref
.set_tcp_keepalive(&keepalive)
.map_err(Error::from)?;
Ok(socket)
}
pub(crate) async fn connect_plain(addr: SocketAddr) -> Result<MaybeTlsStream> {
let socket = connect_socket(addr).await?;
Ok(MaybeTlsStream::Plain(socket))
}
#[cfg(feature = "tls")]
pub(crate) async fn connect_tls(
addr: SocketAddr,
server_name: &str,
config: std::sync::Arc<tokio_rustls::rustls::ClientConfig>,
) -> Result<MaybeTlsStream> {
use tokio_rustls::TlsConnector;
let connector = TlsConnector::from(config);
let socket = connect_socket(addr).await?;
let sni = rustls_pki_types::ServerName::try_from(server_name.to_string())
.map_err(|e| Error::Custom(format!("tcp: invalid SNI {server_name:?}: {e}")))?;
let tls = connector.connect(sni, socket).await.map_err(Error::from)?;
Ok(MaybeTlsStream::Tls(Box::new(tls)))
}
pub(crate) fn split_buffered(
stream: MaybeTlsStream,
) -> (
BufReader<ReadHalf<MaybeTlsStream>>,
BufWriter<WriteHalf<MaybeTlsStream>>,
) {
let (r, w) = tokio::io::split(stream);
(
BufReader::with_capacity(CONN_READ_BUFFER, r),
BufWriter::with_capacity(CONN_WRITE_BUFFER, w),
)
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum ConnectKind {
Plain,
#[cfg(feature = "tls")]
Tls {
server_name: String,
config: std::sync::Arc<tokio_rustls::rustls::ClientConfig>,
},
#[cfg(feature = "tls")]
TlsFailClosed,
}
pub async fn open_handshaken(
addr: SocketAddr,
kind: &ConnectKind,
cfg: &HandshakeConfig,
) -> Result<(MaybeTlsStream, ServerHello)> {
let mut stream = match kind {
ConnectKind::Plain => connect_plain(addr).await?,
#[cfg(feature = "tls")]
ConnectKind::Tls {
server_name,
config,
} => connect_tls(addr, server_name, config.clone()).await?,
#[cfg(feature = "tls")]
ConnectKind::TlsFailClosed => {
return Err(Error::Custom(
"tcp: TLS trust was configured but could not be resolved; \
refusing to connect with default trust"
.into(),
));
}
};
let hello = handshake(&mut stream, cfg).await?;
Ok((stream, hello))
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::net::TcpListener;
#[tokio::test]
async fn connect_plain_sets_nodelay_and_keepalive() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let accept = tokio::spawn(async move { listener.accept().await.unwrap().0 });
let stream = connect_plain(addr).await.expect("loopback connect");
#[cfg_attr(not(feature = "tls"), allow(irrefutable_let_patterns))]
let MaybeTlsStream::Plain(sock) = &stream else {
panic!("connect_plain must yield the plain variant");
};
assert!(sock.nodelay().unwrap(), "TCP_NODELAY must be set");
let sock_ref = socket2::SockRef::from(sock);
assert!(sock_ref.keepalive().unwrap(), "SO_KEEPALIVE must be set");
#[cfg(any(target_os = "linux", target_os = "android"))]
{
assert_eq!(sock_ref.tcp_keepalive_time().unwrap(), TCP_KEEPALIVE_IDLE);
assert_eq!(
sock_ref.tcp_keepalive_interval().unwrap(),
TCP_KEEPALIVE_INTERVAL
);
assert_eq!(
sock_ref.tcp_keepalive_retries().unwrap(),
TCP_KEEPALIVE_RETRIES
);
}
let _accepted = accept.await.unwrap();
}
}