#![forbid(unsafe_code)]
use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use crate::constants::{
SSH_CLIENT_ID, SSH_KEEPALIVE_INTERVAL_SECS, SSH_KEEPALIVE_MAX, SSH_MAX_PACKET_SIZE,
SSH_REKEY_BYTE_LIMIT, SSH_REKEY_TIME_SECS, SSH_WINDOW_SIZE, TCP_KEEPALIVE_ENABLED,
};
use crate::errors::{SshCliError, SshCliResult};
#[must_use]
pub(crate) fn build_ssh_client_config(timeout: Duration) -> Arc<russh::client::Config> {
let mut preferred = russh::Preferred::DEFAULT;
preferred.compression = Cow::Borrowed(&[russh::compression::NONE]);
Arc::new(russh::client::Config {
client_id: russh::SshId::Standard(Cow::Borrowed(SSH_CLIENT_ID)),
limits: russh::Limits::new(
SSH_REKEY_BYTE_LIMIT,
SSH_REKEY_BYTE_LIMIT,
Duration::from_secs(SSH_REKEY_TIME_SECS),
),
window_size: SSH_WINDOW_SIZE,
maximum_packet_size: SSH_MAX_PACKET_SIZE,
inactivity_timeout: Some(timeout),
keepalive_interval: Some(Duration::from_secs(SSH_KEEPALIVE_INTERVAL_SECS)),
keepalive_max: SSH_KEEPALIVE_MAX,
nodelay: true,
preferred,
..Default::default()
})
}
pub(crate) async fn dial_ssh(host: &str, port: u16) -> SshCliResult<TcpStream> {
let socket = crate::net::dial_tcp(host, port).await.map_err(|e| {
SshCliError::ConnectionFailed(format!("TCP dial failed for {host}:{port}: {e}"))
})?;
apply_socket_policy(&socket);
Ok(socket)
}
pub(crate) fn apply_socket_policy(socket: &TcpStream) {
if let Err(e) = socket.set_nodelay(true) {
tracing::debug!(err = %e, "set_nodelay on SSH socket failed");
}
if TCP_KEEPALIVE_ENABLED {
let sock_ref = socket2::SockRef::from(socket);
if let Err(e) = sock_ref.set_keepalive(true) {
tracing::debug!(err = %e, "set_keepalive on SSH socket failed");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_ssh_client_config_policy_surface() {
let cfg = build_ssh_client_config(Duration::from_secs(5));
assert_eq!(cfg.preferred.compression.as_ref(), &[russh::compression::NONE]);
assert!(cfg.nodelay);
assert_eq!(cfg.keepalive_max, SSH_KEEPALIVE_MAX);
assert!(cfg.inactivity_timeout.is_some());
assert!(cfg.keepalive_interval.is_some());
assert_eq!(cfg.window_size, SSH_WINDOW_SIZE);
assert_eq!(cfg.maximum_packet_size, SSH_MAX_PACKET_SIZE);
let id = format!("{:?}", cfg.client_id);
assert!(
id.contains("ssh-cli") || id.contains(SSH_CLIENT_ID),
"client_id should be product-generic: {id}"
);
assert!(
!id.contains("russh_"),
"client_id must not embed russh version: {id}"
);
}
}