Skip to main content

ssh_mcp/ssh/
config.rs

1//! SSH configuration types
2//!
3//! Configuration for SSH connection parameters including authentication.
4
5use std::path::PathBuf;
6
7use clap::ValueEnum;
8
9/// SSH host key verification policy.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
11#[value(rename_all = "kebab-case")]
12pub enum HostKeyCheckMode {
13    /// Require an existing matching known_hosts entry.
14    Yes,
15    /// Learn unknown keys, but reject changed keys.
16    #[default]
17    AcceptNew,
18    /// Disable host key verification.
19    No,
20}
21
22impl HostKeyCheckMode {
23    /// OpenSSH StrictHostKeyChecking value for this policy.
24    pub fn as_openssh_value(self) -> &'static str {
25        match self {
26            Self::Yes => "yes",
27            Self::AcceptNew => "accept-new",
28            Self::No => "no",
29        }
30    }
31}
32
33/// Seconds to send SIGKILL after SIGTERM when using timeout
34pub const TIMEOUT_KILL_AFTER_SECS: u64 = 2;
35
36/// Timeout for timeout command availability detection (ms)
37pub const TIMEOUT_DETECTION_TIMEOUT_MS: u64 = 5000;
38
39/// SSH connection configuration
40#[derive(Debug, Clone)]
41pub struct SshConfig {
42    /// Remote hostname or IP address
43    pub host: String,
44
45    /// SSH port (default: 22)
46    pub port: u16,
47
48    /// Username for authentication
49    pub username: String,
50
51    /// Password for password authentication
52    pub password: Option<String>,
53
54    /// Private key content (not path!) for key authentication
55    pub private_key: Option<String>,
56
57    /// Password for `su` elevation to root
58    pub su_password: Option<String>,
59
60    /// Password for `sudo` commands (if different from su_password)
61    pub sudo_password: Option<String>,
62
63    /// Keepalive interval in seconds (default: 30s)
64    /// Sends keepalive packets to maintain connection like a human user
65    pub keepalive_interval: u64,
66
67    /// Maximum keepalive failures before disconnecting (default: 3)
68    /// How many keepalive packets can be missed before connection drops
69    pub keepalive_max: u64,
70
71    /// Maximum output tokens for command execution (default: 16_000)
72    /// Prevents OOM and context overflow for large outputs
73    pub max_output_tokens: Option<usize>,
74
75    /// Number of reconnect retries after the initial attempt (default: 3)
76    pub reconnect_retries: u64,
77
78    /// Base reconnect backoff in milliseconds (default: 250)
79    pub reconnect_backoff_ms: u64,
80
81    /// Health probe timeout in milliseconds for active session checks (default: 1500)
82    pub health_probe_timeout_ms: u64,
83
84    /// SSH host key verification policy.
85    pub host_key_checking: HostKeyCheckMode,
86
87    /// Optional known_hosts file path.
88    pub known_hosts: Option<PathBuf>,
89}
90
91impl SshConfig {
92    /// Create a new SSH configuration with minimal required fields
93    pub fn new(host: impl Into<String>, username: impl Into<String>) -> Self {
94        Self {
95            host: host.into(),
96            port: 22,
97            username: username.into(),
98            password: None,
99            private_key: None,
100            su_password: None,
101            sudo_password: None,
102            keepalive_interval: 30,
103            keepalive_max: 3,
104            max_output_tokens: Some(16_000),
105            reconnect_retries: 3,
106            reconnect_backoff_ms: 250,
107            health_probe_timeout_ms: 1500,
108            host_key_checking: HostKeyCheckMode::default(),
109            known_hosts: None,
110        }
111    }
112
113    /// Set the SSH port
114    pub fn with_port(mut self, port: u16) -> Self {
115        self.port = port;
116        self
117    }
118
119    /// Set password authentication
120    pub fn with_password(mut self, password: impl Into<String>) -> Self {
121        self.password = Some(password.into());
122        self
123    }
124
125    /// Set private key authentication (key content, not path)
126    pub fn with_private_key(mut self, key: impl Into<String>) -> Self {
127        self.private_key = Some(key.into());
128        self
129    }
130
131    /// Set su password for privilege elevation
132    pub fn with_su_password(mut self, password: impl Into<String>) -> Self {
133        self.su_password = Some(password.into());
134        self
135    }
136
137    /// Set sudo password for sudo commands
138    pub fn with_sudo_password(mut self, password: impl Into<String>) -> Self {
139        self.sudo_password = Some(password.into());
140        self
141    }
142
143    /// Set keepalive interval in seconds (default: 30s)
144    /// Lower values = more frequent keepalives (detect dead connections faster)
145    /// Higher values = less network overhead (more like idle human session)
146    pub fn with_keepalive_interval(mut self, secs: u64) -> Self {
147        self.keepalive_interval = secs;
148        self
149    }
150
151    /// Set maximum keepalive failures before disconnecting (default: 3)
152    /// Total idle timeout = keepalive_interval * keepalive_max
153    /// Example: 30s * 3 = 90s of inactivity before disconnect
154    pub fn with_keepalive_max(mut self, max: u64) -> Self {
155        self.keepalive_max = max;
156        self
157    }
158
159    /// Set maximum output tokens for command execution (default: 16_000)
160    /// Set to None for unlimited output (not recommended for large outputs)
161    pub fn with_max_output_tokens(mut self, tokens: Option<usize>) -> Self {
162        self.max_output_tokens = tokens;
163        self
164    }
165
166    /// Set reconnect retries after the initial attempt (default: 3)
167    pub fn with_reconnect_retries(mut self, retries: u64) -> Self {
168        self.reconnect_retries = retries;
169        self
170    }
171
172    /// Set base reconnect backoff in milliseconds (default: 250)
173    pub fn with_reconnect_backoff_ms(mut self, backoff_ms: u64) -> Self {
174        self.reconnect_backoff_ms = backoff_ms;
175        self
176    }
177
178    /// Set health probe timeout in milliseconds (default: 1500)
179    pub fn with_health_probe_timeout_ms(mut self, timeout_ms: u64) -> Self {
180        self.health_probe_timeout_ms = timeout_ms;
181        self
182    }
183
184    /// Set SSH host key verification policy.
185    pub fn with_host_key_checking(mut self, mode: HostKeyCheckMode) -> Self {
186        self.host_key_checking = mode;
187        self
188    }
189
190    /// Set a custom known_hosts file path.
191    pub fn with_known_hosts(mut self, known_hosts: Option<PathBuf>) -> Self {
192        self.known_hosts = known_hosts;
193        self
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_ssh_config_builder() {
203        let config = SshConfig::new("192.168.1.1", "admin")
204            .with_port(2222)
205            .with_password("secret")
206            .with_max_output_tokens(Some(5_000))
207            .with_reconnect_retries(4)
208            .with_reconnect_backoff_ms(500)
209            .with_health_probe_timeout_ms(1_200);
210
211        assert_eq!(config.host, "192.168.1.1");
212        assert_eq!(config.port, 2222);
213        assert_eq!(config.username, "admin");
214        assert_eq!(config.password, Some("secret".to_string()));
215        assert!(config.private_key.is_none());
216        assert_eq!(config.reconnect_retries, 4);
217        assert_eq!(config.reconnect_backoff_ms, 500);
218        assert_eq!(config.health_probe_timeout_ms, 1_200);
219        assert_eq!(config.host_key_checking, HostKeyCheckMode::AcceptNew);
220        assert!(config.known_hosts.is_none());
221    }
222}