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}