ssh.rs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. use anyhow::{Context, Result};
  2. use base64::Engine;
  3. use base64::engine::general_purpose::STANDARD;
  4. use russh::keys::{PrivateKeyWithHashAlg, load_secret_key as russh_load_secret_key};
  5. use russh::*;
  6. use sha2::{Digest, Sha256};
  7. use std::sync::Arc;
  8. use tokio::time::{Duration, sleep};
  9. use tracing::warn;
  10. use crate::config::{Config, Node};
  11. /// Establishes an SSH session to a node using the config credentials.
  12. pub async fn ssh_session(config: &Config, node: &Node) -> Result<client::Handle<VerifyingHandler>> {
  13. let key = load_secret_key(&config.ssh_key_path)?;
  14. let ssh_config = russh::client::Config::default();
  15. let expected_key = Arc::new(std::sync::Mutex::new(node.host_key.clone()));
  16. if expected_key.lock().unwrap().is_none() {
  17. warn!(
  18. "No host_key set for {} — skipping verification (insecure)",
  19. node.name
  20. );
  21. }
  22. let handler = VerifyingHandler { expected_key };
  23. let mut session =
  24. russh::client::connect(Arc::new(ssh_config), (node.ssh_ip.as_str(), 22), handler).await?;
  25. session
  26. .authenticate_publickey(
  27. &config.ssh_user,
  28. PrivateKeyWithHashAlg::new(Arc::new(key), None),
  29. )
  30. .await?;
  31. Ok(session)
  32. }
  33. /// Runs a command on a node via SSH and returns stdout.
  34. ///
  35. /// The remote command's exit status is tracked: if it is non-zero,
  36. /// an Err is returned whose `to_string()` includes the captured output
  37. /// for diagnosis. Callers that don't care about exit status can ignore
  38. /// the Result and inspect stdout via the returned `String`.
  39. pub async fn run_ssh(
  40. config: &Config,
  41. node: &Node,
  42. command: String,
  43. dry_run: bool,
  44. ) -> Result<String> {
  45. if dry_run {
  46. println!(" ssh {}@{}: {}", config.ssh_user, node.ssh_ip, command);
  47. return Ok(String::new());
  48. }
  49. let session = ssh_session(config, node).await?;
  50. let mut channel = session.channel_open_session().await?;
  51. channel.exec(true, command.as_bytes()).await?;
  52. let mut output = String::new();
  53. let mut exit_status: Option<u32> = None;
  54. while let Some(msg) = channel.wait().await {
  55. match msg {
  56. ChannelMsg::Data { data } => {
  57. output.push_str(&String::from_utf8_lossy(&data));
  58. }
  59. ChannelMsg::ExitStatus { exit_status: s } => {
  60. exit_status = Some(s);
  61. }
  62. _ => {}
  63. }
  64. }
  65. match exit_status {
  66. Some(0) | None => Ok(output),
  67. Some(code) => Err(anyhow::anyhow!(
  68. "ssh {}@{}: command exited with status {}\n command: {}\n output:\n{}",
  69. config.ssh_user,
  70. node.ssh_ip,
  71. code,
  72. command,
  73. output
  74. )),
  75. }
  76. }
  77. /// Runs a command via SSH with exponential backoff retry.
  78. pub async fn run_ssh_with_retry(
  79. config: &Config,
  80. node: &Node,
  81. command: String,
  82. dry_run: bool,
  83. max_retries: u32,
  84. ) -> Result<String> {
  85. let mut last_err = None;
  86. for attempt in 0..=max_retries {
  87. match run_ssh(config, node, command.clone(), dry_run).await {
  88. Ok(output) => return Ok(output),
  89. Err(e) => {
  90. warn!(
  91. "SSH attempt {}/{} to {} failed: {}",
  92. attempt + 1,
  93. max_retries + 1,
  94. node.name,
  95. e
  96. );
  97. last_err = Some(e);
  98. if attempt < max_retries {
  99. sleep(Duration::from_secs(2u64.pow(attempt))).await;
  100. }
  101. }
  102. }
  103. }
  104. Err(last_err.unwrap())
  105. }
  106. /// Runs a command via SSH and streams output to stdout (for follow mode).
  107. pub async fn run_ssh_follow(
  108. config: &Config,
  109. node: &Node,
  110. command: String,
  111. dry_run: bool,
  112. ) -> Result<()> {
  113. if dry_run {
  114. println!(" ssh {}@{}: {} -f", config.ssh_user, node.ssh_ip, command);
  115. return Ok(());
  116. }
  117. let session = ssh_session(config, node).await?;
  118. let mut channel = session.channel_open_session().await?;
  119. channel.exec(true, command.as_bytes()).await?;
  120. while let Some(msg) = channel.wait().await {
  121. if let ChannelMsg::Data { data } = msg {
  122. print!("{}", String::from_utf8_lossy(&data));
  123. }
  124. }
  125. Ok(())
  126. }
  127. /// Fetches the SSH host key fingerprint from a node.
  128. pub async fn fetch_host_key(config: &Config, node: &Node) -> Result<String> {
  129. use std::sync::Mutex;
  130. let captured = Arc::new(Mutex::new(None::<Vec<u8>>));
  131. let captured_clone = captured.clone();
  132. struct CaptureHandler {
  133. captured: Arc<Mutex<Option<Vec<u8>>>>,
  134. }
  135. impl client::Handler for CaptureHandler {
  136. type Error = russh::Error;
  137. async fn check_server_key(
  138. &mut self,
  139. key: &russh::keys::PublicKey,
  140. ) -> Result<bool, Self::Error> {
  141. *self.captured.lock().unwrap() = Some(key.to_string().into_bytes());
  142. Ok(true)
  143. }
  144. }
  145. let key = load_secret_key(&config.ssh_key_path)?;
  146. let ssh_config = russh::client::Config::default();
  147. let handler = CaptureHandler {
  148. captured: captured_clone,
  149. };
  150. let mut session =
  151. russh::client::connect(Arc::new(ssh_config), (node.ssh_ip.as_str(), 22), handler).await?;
  152. session
  153. .authenticate_publickey(
  154. &config.ssh_user,
  155. PrivateKeyWithHashAlg::new(Arc::new(key), None),
  156. )
  157. .await?;
  158. let blob = captured
  159. .lock()
  160. .unwrap()
  161. .take()
  162. .context("No server key captured")?;
  163. let hash = Sha256::digest(&blob);
  164. let fp = format!("SHA256:{}", STANDARD.encode(hash));
  165. Ok(fp)
  166. }
  167. /// Loads an SSH private key from the given path.
  168. pub fn load_secret_key(path: &str) -> Result<russh::keys::PrivateKey> {
  169. russh_load_secret_key(path, None).context("Failed to load SSH key")
  170. }
  171. /// Handler that verifies the server's host key against an expected fingerprint.
  172. pub struct VerifyingHandler {
  173. pub expected_key: Arc<std::sync::Mutex<Option<String>>>,
  174. }
  175. impl client::Handler for VerifyingHandler {
  176. type Error = russh::Error;
  177. async fn check_server_key(
  178. &mut self,
  179. server_public_key: &russh::keys::PublicKey,
  180. ) -> Result<bool, Self::Error> {
  181. let expected = self.expected_key.lock().unwrap().clone();
  182. if let Some(ref expected_fp) = expected {
  183. let key_bytes = server_public_key.to_string().into_bytes();
  184. let hash = Sha256::digest(&key_bytes);
  185. let fp = format!("SHA256:{}", STANDARD.encode(hash));
  186. if &fp != expected_fp {
  187. return Err(russh::Error::Kex);
  188. }
  189. }
  190. Ok(true)
  191. }
  192. }