Skip to main content

ssh_cli/tunnel/
streamlocal.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// G-SECDEV-05: pure module — no `unsafe`.
3#![forbid(unsafe_code)]
4//! Validation for `tunnel --remote-socket` (G-TUN-R03).
5//!
6//! The forwarding itself reuses the local accept loop — a Unix socket target only
7//! changes which SSH channel type is opened. What is specific to this mode is the
8//! *precondition*: the path names a socket on the remote host, so it must be
9//! judged by the remote's rules, not this machine's.
10//!
11//! # Platform note
12//!
13//! The **client** may run on Windows: it only ever speaks TCP locally and asks the
14//! server to reach the socket. The gate that matters is the server's support for
15//! the `direct-streamlocal@openssh.com` extension, which is detected on the wire
16//! rather than guessed from the local platform.
17
18use crate::errors::{SshCliError, SshCliResult};
19
20/// Rejects a remote socket path that cannot be valid on any POSIX host.
21///
22/// Deliberately minimal: the socket lives on the *server*, so anything beyond
23/// clearly-impossible input would be this machine second-guessing a filesystem it
24/// cannot see. Checking `Path::exists` locally would be actively wrong — it would
25/// pass or fail based on paths that have nothing to do with the remote host.
26///
27/// # Errors
28/// [`SshCliError::InvalidArgument`] (exit 64) for an empty path, a relative path,
29/// or one containing a NUL byte.
30pub fn validate_remote_socket(path: &str) -> SshCliResult<()> {
31    if path.is_empty() {
32        return Err(SshCliError::InvalidArgument(
33            "--remote-socket requires a path".to_string(),
34        ));
35    }
36    if !path.starts_with('/') {
37        return Err(SshCliError::InvalidArgument(format!(
38            "--remote-socket must be an absolute remote path, got `{path}`"
39        )));
40    }
41    if path.contains('\0') {
42        return Err(SshCliError::InvalidArgument(
43            "--remote-socket must not contain a NUL byte".to_string(),
44        ));
45    }
46    Ok(())
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn absolute_socket_path_is_accepted() {
55        assert!(validate_remote_socket("/var/run/docker.sock").is_ok());
56    }
57
58    #[test]
59    fn empty_path_is_rejected() {
60        let err = validate_remote_socket("").expect_err("empty path must be rejected");
61        assert_eq!(err.exit_code(), crate::errors::exit_codes::EX_USAGE);
62    }
63
64    #[test]
65    fn relative_path_is_rejected() {
66        // A relative path would be resolved against the server's cwd, which this
67        // side cannot know — so it can never mean what the caller intended.
68        let err =
69            validate_remote_socket("run/docker.sock").expect_err("relative path must be rejected");
70        assert!(matches!(err, SshCliError::InvalidArgument(_)));
71    }
72
73    #[test]
74    fn nul_byte_is_rejected() {
75        assert!(validate_remote_socket("/var/run/x\0y.sock").is_err());
76    }
77
78    #[test]
79    fn windows_style_path_is_rejected_because_the_target_is_posix() {
80        // The client may run on Windows, but the socket lives on the remote host.
81        assert!(validate_remote_socket("C:\\pipe\\docker").is_err());
82    }
83}