Skip to main content

ssh2_config/
params.rs

1//! # params
2//!
3//! Ssh config params for host rule
4
5mod algos;
6mod remote_forward;
7
8use std::collections::HashMap;
9
10pub use self::algos::Algorithms;
11pub(crate) use self::algos::AlgorithmsRule;
12pub use self::remote_forward::{RemoteForward, RemoteForwardDestination, RemoteForwardListen};
13use super::{Duration, PathBuf};
14use crate::DefaultAlgorithms;
15
16/// Describes the ssh configuration.
17/// Configuration is described in this document: <http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5>
18/// Only arguments supported by libssh2 are implemented
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct HostParams {
21    /// Specifies whether keys should be automatically added to a running ssh-agent(1)
22    pub add_keys_to_agent: Option<bool>,
23    /// Specifies to use the specified address on the local machine as the source address of the connection
24    pub bind_address: Option<String>,
25    /// Use the specified address on the local machine as the source address of the connection
26    pub bind_interface: Option<String>,
27    /// Specifies which algorithms are allowed for signing of certificates by certificate authorities
28    pub ca_signature_algorithms: Algorithms,
29    /// Specifies a file from which the user's certificate is read
30    pub certificate_file: Option<PathBuf>,
31    /// Specifies the ciphers allowed for protocol version 2 in order of preference
32    pub ciphers: Algorithms,
33    /// Specifies whether to use compression
34    pub compression: Option<bool>,
35    /// Specifies the number of attempts to make before exiting
36    pub connection_attempts: Option<usize>,
37    /// Specifies the timeout used when connecting to the SSH server
38    pub connect_timeout: Option<Duration>,
39    /// Specifies whether the connection to the authentication agent (if any) will be forwarded to the remote machine
40    pub forward_agent: Option<bool>,
41    /// Specifies the host key signature algorithms that the client wants to use in order of preference
42    pub host_key_algorithms: Algorithms,
43    /// Specifies the real host name to log into
44    pub host_name: Option<String>,
45    /// Specifies the path of the identity file to be used when authenticating.
46    /// More than one file can be specified.
47    /// If more than one file is specified, they will be read in order.
48    /// There may be multiple lines of this directive; in case subsequent lines will be appended to the previous ones.
49    pub identity_file: Option<Vec<PathBuf>>,
50    /// Specifies a pattern-list of unknown options to be ignored if they are encountered in configuration parsing
51    pub ignore_unknown: Option<Vec<String>>,
52    /// Specifies the available KEX (Key Exchange) algorithms
53    pub kex_algorithms: Algorithms,
54    /// Specifies the MAC (message authentication code) algorithms in order of preference
55    pub mac: Algorithms,
56    /// Specifies the port number to connect on the remote host.
57    pub port: Option<u16>,
58    /// Specifies one or more jump proxies as either \[user@\]host\[:port\] or an ssh URI
59    pub proxy_jump: Option<Vec<String>>,
60    /// Specifies the signature algorithms that will be used for public key authentication
61    pub pubkey_accepted_algorithms: Algorithms,
62    /// Specifies whether to try public key authentication using SSH keys
63    pub pubkey_authentication: Option<bool>,
64    /// Specifies remote forwarding listeners and their optional destinations.
65    pub remote_forward: Vec<RemoteForward>,
66    /// Sets a timeout interval in seconds after which if no data has been received from the server, keep alive will be sent
67    pub server_alive_interval: Option<Duration>,
68    /// Specifies whether to send TCP keepalives to the other side
69    pub tcp_keep_alive: Option<bool>,
70    #[cfg(target_os = "macos")]
71    /// specifies whether the system should search for passphrases in the user's keychain when attempting to use a particular key
72    pub use_keychain: Option<bool>,
73    /// Specifies the user to log in as.
74    pub user: Option<String>,
75    /// fields that the parser wasn't able to parse
76    pub ignored_fields: HashMap<String, Vec<String>>,
77    /// fields that the parser was able to parse but ignored
78    pub unsupported_fields: HashMap<String, Vec<String>>,
79}
80
81impl HostParams {
82    /// Create a new [`HostParams`] object with the [`DefaultAlgorithms`]
83    pub fn new(default_algorithms: &DefaultAlgorithms) -> Self {
84        Self {
85            add_keys_to_agent: None,
86            bind_address: None,
87            bind_interface: None,
88            ca_signature_algorithms: Algorithms::new(&default_algorithms.ca_signature_algorithms),
89            certificate_file: None,
90            ciphers: Algorithms::new(&default_algorithms.ciphers),
91            compression: None,
92            connection_attempts: None,
93            connect_timeout: None,
94            forward_agent: None,
95            host_key_algorithms: Algorithms::new(&default_algorithms.host_key_algorithms),
96            host_name: None,
97            identity_file: None,
98            ignore_unknown: None,
99            kex_algorithms: Algorithms::new(&default_algorithms.kex_algorithms),
100            mac: Algorithms::new(&default_algorithms.mac),
101            port: None,
102            proxy_jump: None,
103            pubkey_accepted_algorithms: Algorithms::new(
104                &default_algorithms.pubkey_accepted_algorithms,
105            ),
106            pubkey_authentication: None,
107            remote_forward: Vec::new(),
108            server_alive_interval: None,
109            tcp_keep_alive: None,
110            #[cfg(target_os = "macos")]
111            use_keychain: None,
112            user: None,
113            ignored_fields: HashMap::new(),
114            unsupported_fields: HashMap::new(),
115        }
116    }
117
118    /// Return whether a certain `param` is in the ignored list
119    pub(crate) fn ignored(&self, param: &str) -> bool {
120        self.ignore_unknown
121            .as_ref()
122            .map(|x| x.iter().any(|x| x.as_str() == param))
123            .unwrap_or(false)
124    }
125
126    /// Given a [`HostParams`] object `b`, it will overwrite all the params from `self` only if they are [`None`]
127    pub fn overwrite_if_none(&mut self, b: &Self) {
128        self.add_keys_to_agent = self.add_keys_to_agent.or(b.add_keys_to_agent);
129        self.bind_address = self.bind_address.clone().or_else(|| b.bind_address.clone());
130        self.bind_interface = self
131            .bind_interface
132            .clone()
133            .or_else(|| b.bind_interface.clone());
134        self.certificate_file = self
135            .certificate_file
136            .clone()
137            .or_else(|| b.certificate_file.clone());
138        self.compression = self.compression.or(b.compression);
139        self.connection_attempts = self.connection_attempts.or(b.connection_attempts);
140        self.connect_timeout = self.connect_timeout.or(b.connect_timeout);
141        self.forward_agent = self.forward_agent.or(b.forward_agent);
142        self.host_name = self.host_name.clone().or_else(|| b.host_name.clone());
143        // IdentityFile accumulates across Host blocks (unlike other directives)
144        match (&mut self.identity_file, &b.identity_file) {
145            (Some(existing), Some(other)) => existing.extend(other.clone()),
146            (None, Some(other)) => self.identity_file = Some(other.clone()),
147            _ => {}
148        }
149        self.ignore_unknown = self
150            .ignore_unknown
151            .clone()
152            .or_else(|| b.ignore_unknown.clone());
153        self.port = self.port.or(b.port);
154        self.proxy_jump = self.proxy_jump.clone().or_else(|| b.proxy_jump.clone());
155        self.pubkey_authentication = self.pubkey_authentication.or(b.pubkey_authentication);
156        self.remote_forward.extend_from_slice(&b.remote_forward);
157        self.server_alive_interval = self.server_alive_interval.or(b.server_alive_interval);
158        #[cfg(target_os = "macos")]
159        {
160            self.use_keychain = self.use_keychain.or(b.use_keychain);
161        }
162        self.tcp_keep_alive = self.tcp_keep_alive.or(b.tcp_keep_alive);
163        self.user = self.user.clone().or_else(|| b.user.clone());
164        for (ignored_field, args) in &b.ignored_fields {
165            if !self.ignored_fields.contains_key(ignored_field) {
166                self.ignored_fields
167                    .insert(ignored_field.to_owned(), args.to_owned());
168            }
169        }
170        for (unsupported_field, args) in &b.unsupported_fields {
171            if !self.unsupported_fields.contains_key(unsupported_field) {
172                self.unsupported_fields
173                    .insert(unsupported_field.to_owned(), args.to_owned());
174            }
175        }
176
177        // merge algos if default and b is not default
178        if self.ca_signature_algorithms.is_default() && !b.ca_signature_algorithms.is_default() {
179            self.ca_signature_algorithms = b.ca_signature_algorithms.clone();
180        }
181        if self.ciphers.is_default() && !b.ciphers.is_default() {
182            self.ciphers = b.ciphers.clone();
183        }
184        if self.host_key_algorithms.is_default() && !b.host_key_algorithms.is_default() {
185            self.host_key_algorithms = b.host_key_algorithms.clone();
186        }
187        if self.kex_algorithms.is_default() && !b.kex_algorithms.is_default() {
188            self.kex_algorithms = b.kex_algorithms.clone();
189        }
190        if self.mac.is_default() && !b.mac.is_default() {
191            self.mac = b.mac.clone();
192        }
193        if self.pubkey_accepted_algorithms.is_default()
194            && !b.pubkey_accepted_algorithms.is_default()
195        {
196            self.pubkey_accepted_algorithms = b.pubkey_accepted_algorithms.clone();
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203
204    use std::path::PathBuf;
205    use std::str::FromStr;
206
207    use pretty_assertions::assert_eq;
208
209    use super::*;
210    use crate::params::algos::AlgorithmsRule;
211
212    #[test]
213    fn should_model_remote_forward_endpoints() {
214        let port = RemoteForward::new(RemoteForwardListen::Port(8080), None);
215        assert_eq!(port.to_string(), "8080");
216
217        let host = RemoteForward::new(
218            RemoteForwardListen::Host {
219                host: "localhost".to_string(),
220                port: 8080,
221            },
222            Some(RemoteForwardDestination::Host {
223                host: "127.0.0.1".to_string(),
224                port: 80,
225            }),
226        );
227        assert_eq!(host.to_string(), "localhost:8080 127.0.0.1:80");
228
229        let socket = RemoteForward::new(
230            RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote socket")),
231            Some(RemoteForwardDestination::UnixSocket(PathBuf::from(
232                "/tmp/local.sock",
233            ))),
234        );
235        assert_eq!(socket.to_string(), "\"/tmp/remote socket\" /tmp/local.sock");
236
237        let ipv6 = RemoteForward::new(
238            RemoteForwardListen::Host {
239                host: "::1".to_string(),
240                port: 8080,
241            },
242            None,
243        );
244        assert_eq!(ipv6.to_string(), "[::1]:8080");
245    }
246
247    #[test]
248    fn should_initialize_params() {
249        let params = HostParams::new(&DefaultAlgorithms::default());
250        assert!(params.add_keys_to_agent.is_none());
251        assert!(params.bind_address.is_none());
252        assert!(params.bind_interface.is_none());
253        assert_eq!(
254            params.ca_signature_algorithms.algorithms(),
255            DefaultAlgorithms::default().ca_signature_algorithms
256        );
257        assert!(params.certificate_file.is_none());
258        assert_eq!(
259            params.ciphers.algorithms(),
260            DefaultAlgorithms::default().ciphers
261        );
262        assert!(params.compression.is_none());
263        assert!(params.connection_attempts.is_none());
264        assert!(params.connect_timeout.is_none());
265        assert!(params.forward_agent.is_none());
266        assert_eq!(
267            params.host_key_algorithms.algorithms(),
268            DefaultAlgorithms::default().host_key_algorithms
269        );
270        assert!(params.host_name.is_none());
271        assert!(params.identity_file.is_none());
272        assert!(params.ignore_unknown.is_none());
273        assert_eq!(
274            params.kex_algorithms.algorithms(),
275            DefaultAlgorithms::default().kex_algorithms
276        );
277        assert_eq!(params.mac.algorithms(), DefaultAlgorithms::default().mac);
278        assert!(params.port.is_none());
279        assert!(params.proxy_jump.is_none());
280        assert_eq!(
281            params.pubkey_accepted_algorithms.algorithms(),
282            DefaultAlgorithms::default().pubkey_accepted_algorithms
283        );
284        assert!(params.pubkey_authentication.is_none());
285        assert!(params.remote_forward.is_empty());
286        assert!(params.server_alive_interval.is_none());
287        #[cfg(target_os = "macos")]
288        assert!(params.use_keychain.is_none());
289        assert!(params.tcp_keep_alive.is_none());
290    }
291
292    #[test]
293    fn test_should_overwrite_if_none() {
294        let mut params = HostParams::new(&DefaultAlgorithms::default());
295        params.bind_address = Some(String::from("pippo"));
296
297        let mut b = HostParams::new(&DefaultAlgorithms::default());
298        b.bind_address = Some(String::from("pluto"));
299        b.bind_interface = Some(String::from("tun0"));
300        b.ciphers
301            .apply(AlgorithmsRule::from_str("c,d").expect("parse error"));
302
303        params.overwrite_if_none(&b);
304        assert_eq!(params.bind_address.unwrap(), "pippo");
305        assert_eq!(params.bind_interface.unwrap(), "tun0");
306
307        // algos
308        assert_eq!(
309            params.ciphers.algorithms(),
310            vec!["c".to_string(), "d".to_string()]
311        );
312    }
313
314    #[test]
315    fn test_ignored_returns_false_when_none() {
316        let params = HostParams::new(&DefaultAlgorithms::default());
317        assert!(!params.ignored("SomeParam"));
318    }
319
320    #[test]
321    fn test_ignored_returns_false_when_not_in_list() {
322        let mut params = HostParams::new(&DefaultAlgorithms::default());
323        params.ignore_unknown = Some(vec!["Param1".to_string(), "Param2".to_string()]);
324        assert!(!params.ignored("OtherParam"));
325    }
326
327    #[test]
328    fn test_ignored_returns_true_when_in_list() {
329        let mut params = HostParams::new(&DefaultAlgorithms::default());
330        params.ignore_unknown = Some(vec!["Param1".to_string(), "Param2".to_string()]);
331        assert!(params.ignored("Param1"));
332        assert!(params.ignored("Param2"));
333    }
334
335    #[test]
336    fn test_overwrite_if_none_all_fields() {
337        let mut params = HostParams::new(&DefaultAlgorithms::empty());
338
339        let mut b = HostParams::new(&DefaultAlgorithms::empty());
340        b.add_keys_to_agent = Some(true);
341        b.bind_address = Some(String::from("addr"));
342        b.bind_interface = Some(String::from("iface"));
343        b.certificate_file = Some(std::path::PathBuf::from("/cert"));
344        b.compression = Some(true);
345        b.connection_attempts = Some(5);
346        b.connect_timeout = Some(Duration::from_secs(30));
347        b.forward_agent = Some(true);
348        b.host_name = Some(String::from("host"));
349        b.identity_file = Some(vec![std::path::PathBuf::from("/id")]);
350        b.ignore_unknown = Some(vec!["field".to_string()]);
351        b.port = Some(22);
352        b.proxy_jump = Some(vec!["proxy".to_string()]);
353        b.pubkey_authentication = Some(true);
354        b.remote_forward = vec![RemoteForward::new(RemoteForwardListen::Port(8080), None)];
355        b.server_alive_interval = Some(Duration::from_secs(60));
356        b.tcp_keep_alive = Some(true);
357        #[cfg(target_os = "macos")]
358        {
359            b.use_keychain = Some(true);
360        }
361        b.user = Some(String::from("user"));
362        b.ignored_fields
363            .insert("custom".to_string(), vec!["value".to_string()]);
364        b.unsupported_fields
365            .insert("unsupported".to_string(), vec!["val".to_string()]);
366
367        params.overwrite_if_none(&b);
368
369        assert_eq!(params.add_keys_to_agent, Some(true));
370        assert_eq!(params.bind_address, Some(String::from("addr")));
371        assert_eq!(params.bind_interface, Some(String::from("iface")));
372        assert_eq!(
373            params.certificate_file,
374            Some(std::path::PathBuf::from("/cert"))
375        );
376        assert_eq!(params.compression, Some(true));
377        assert_eq!(params.connection_attempts, Some(5));
378        assert_eq!(params.connect_timeout, Some(Duration::from_secs(30)));
379        assert_eq!(params.forward_agent, Some(true));
380        assert_eq!(params.host_name, Some(String::from("host")));
381        assert_eq!(
382            params.identity_file,
383            Some(vec![std::path::PathBuf::from("/id")])
384        );
385        assert_eq!(params.ignore_unknown, Some(vec!["field".to_string()]));
386        assert_eq!(params.port, Some(22));
387        assert_eq!(params.proxy_jump, Some(vec!["proxy".to_string()]));
388        assert_eq!(params.pubkey_authentication, Some(true));
389        assert_eq!(
390            params.remote_forward,
391            vec![RemoteForward::new(RemoteForwardListen::Port(8080), None,)]
392        );
393        assert_eq!(params.server_alive_interval, Some(Duration::from_secs(60)));
394        assert_eq!(params.tcp_keep_alive, Some(true));
395        #[cfg(target_os = "macos")]
396        assert_eq!(params.use_keychain, Some(true));
397        assert_eq!(params.user, Some(String::from("user")));
398        assert!(params.ignored_fields.contains_key("custom"));
399        assert!(params.unsupported_fields.contains_key("unsupported"));
400    }
401
402    #[test]
403    fn test_overwrite_if_none_does_not_overwrite_existing() {
404        let mut params = HostParams::new(&DefaultAlgorithms::empty());
405        params.add_keys_to_agent = Some(false);
406        params.bind_address = Some(String::from("original"));
407        params.compression = Some(false);
408        params.port = Some(2222);
409        params.user = Some(String::from("original_user"));
410        params
411            .ignored_fields
412            .insert("existing".to_string(), vec!["val1".to_string()]);
413        params
414            .unsupported_fields
415            .insert("existing_unsup".to_string(), vec!["val1".to_string()]);
416
417        let mut b = HostParams::new(&DefaultAlgorithms::empty());
418        b.add_keys_to_agent = Some(true);
419        b.bind_address = Some(String::from("new"));
420        b.compression = Some(true);
421        b.port = Some(22);
422        b.user = Some(String::from("new_user"));
423        b.ignored_fields
424            .insert("existing".to_string(), vec!["val2".to_string()]);
425        b.unsupported_fields
426            .insert("existing_unsup".to_string(), vec!["val2".to_string()]);
427
428        params.overwrite_if_none(&b);
429
430        // Should keep original values
431        assert_eq!(params.add_keys_to_agent, Some(false));
432        assert_eq!(params.bind_address, Some(String::from("original")));
433        assert_eq!(params.compression, Some(false));
434        assert_eq!(params.port, Some(2222));
435        assert_eq!(params.user, Some(String::from("original_user")));
436        assert_eq!(
437            params.ignored_fields.get("existing"),
438            Some(&vec!["val1".to_string()])
439        );
440        assert_eq!(
441            params.unsupported_fields.get("existing_unsup"),
442            Some(&vec!["val1".to_string()])
443        );
444    }
445
446    #[test]
447    fn should_accumulate_remote_forwards_when_merging() {
448        let mut params = HostParams::new(&DefaultAlgorithms::empty());
449        params.remote_forward = vec![RemoteForward::new(RemoteForwardListen::Port(8080), None)];
450        let mut other = HostParams::new(&DefaultAlgorithms::empty());
451        other.remote_forward = vec![RemoteForward::new(
452            RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
453            None,
454        )];
455
456        params.overwrite_if_none(&other);
457
458        assert_eq!(
459            params.remote_forward,
460            vec![
461                RemoteForward::new(RemoteForwardListen::Port(8080), None),
462                RemoteForward::new(
463                    RemoteForwardListen::UnixSocket(PathBuf::from("/tmp/remote.sock")),
464                    None,
465                ),
466            ]
467        );
468    }
469
470    #[test]
471    fn test_overwrite_if_none_algorithms_when_self_is_default() {
472        let mut params = HostParams::new(&DefaultAlgorithms::empty());
473
474        let mut b = HostParams::new(&DefaultAlgorithms::empty());
475        b.ca_signature_algorithms
476            .apply(AlgorithmsRule::from_str("ca-algo").expect("parse error"));
477        b.host_key_algorithms
478            .apply(AlgorithmsRule::from_str("hk-algo").expect("parse error"));
479        b.kex_algorithms
480            .apply(AlgorithmsRule::from_str("kex-algo").expect("parse error"));
481        b.mac
482            .apply(AlgorithmsRule::from_str("mac-algo").expect("parse error"));
483        b.pubkey_accepted_algorithms
484            .apply(AlgorithmsRule::from_str("pk-algo").expect("parse error"));
485
486        params.overwrite_if_none(&b);
487
488        assert_eq!(
489            params.ca_signature_algorithms.algorithms(),
490            &["ca-algo".to_string()]
491        );
492        assert_eq!(
493            params.host_key_algorithms.algorithms(),
494            &["hk-algo".to_string()]
495        );
496        assert_eq!(
497            params.kex_algorithms.algorithms(),
498            &["kex-algo".to_string()]
499        );
500        assert_eq!(params.mac.algorithms(), &["mac-algo".to_string()]);
501        assert_eq!(
502            params.pubkey_accepted_algorithms.algorithms(),
503            &["pk-algo".to_string()]
504        );
505    }
506
507    #[test]
508    fn test_overwrite_if_none_algorithms_when_self_is_not_default() {
509        let mut params = HostParams::new(&DefaultAlgorithms::empty());
510        params
511            .ciphers
512            .apply(AlgorithmsRule::from_str("self-cipher").expect("parse error"));
513
514        let mut b = HostParams::new(&DefaultAlgorithms::empty());
515        b.ciphers
516            .apply(AlgorithmsRule::from_str("other-cipher").expect("parse error"));
517
518        params.overwrite_if_none(&b);
519
520        // Self's cipher should remain since it was already overridden
521        assert_eq!(params.ciphers.algorithms(), &["self-cipher".to_string()]);
522    }
523
524    #[test]
525    fn test_overwrite_if_none_accumulates_identity_files() {
526        let mut params = HostParams::new(&DefaultAlgorithms::empty());
527        params.identity_file = Some(vec![std::path::PathBuf::from("/path/to/key1")]);
528
529        let mut b = HostParams::new(&DefaultAlgorithms::empty());
530        b.identity_file = Some(vec![
531            std::path::PathBuf::from("/path/to/key2"),
532            std::path::PathBuf::from("/path/to/key3"),
533        ]);
534
535        params.overwrite_if_none(&b);
536
537        // Identity files should be accumulated, not replaced
538        assert_eq!(
539            params.identity_file,
540            Some(vec![
541                std::path::PathBuf::from("/path/to/key1"),
542                std::path::PathBuf::from("/path/to/key2"),
543                std::path::PathBuf::from("/path/to/key3"),
544            ])
545        );
546    }
547
548    #[test]
549    fn test_overwrite_if_none_identity_files_when_self_is_none() {
550        let mut params = HostParams::new(&DefaultAlgorithms::empty());
551
552        let mut b = HostParams::new(&DefaultAlgorithms::empty());
553        b.identity_file = Some(vec![std::path::PathBuf::from("/path/to/key1")]);
554
555        params.overwrite_if_none(&b);
556
557        assert_eq!(
558            params.identity_file,
559            Some(vec![std::path::PathBuf::from("/path/to/key1")])
560        );
561    }
562}