Skip to main content

bssh/forwarding/
mod.rs

1//! Port forwarding implementation for bssh
2//!
3//! This module provides comprehensive SSH port forwarding capabilities including:
4//! - Local port forwarding (-L): Forward local port to remote destination via SSH
5//! - Remote port forwarding (-R): Forward remote port to local destination via SSH  
6//! - Dynamic port forwarding (-D): SOCKS proxy for dynamic destination forwarding
7//!
8//! # Architecture
9//!
10//! The forwarding system is built around three core components:
11//! - **ForwardingSpec**: Parsing and validation of forwarding specifications
12//! - **ForwardingManager**: Lifecycle management and coordination of forwards
13//! - **Forwarder**: Individual forwarding implementations (local, remote, dynamic)
14//!
15//! # Design Principles
16//!
17//! - **Async-first**: Built on Tokio for maximum concurrency and performance
18//! - **Resource-managed**: Proper cleanup and error handling with RAII patterns
19//! - **Multiplexed**: Multiple forwards over single SSH connection when possible
20//! - **Resilient**: Automatic reconnection with exponential backoff
21//! - **Observable**: Comprehensive status reporting and monitoring
22
23pub mod dynamic;
24pub mod local;
25pub mod manager;
26pub mod remote;
27pub mod spec;
28pub mod tunnel;
29
30// Re-export key types for convenience
31pub use manager::{ForwardingId, ForwardingManager, ForwardingMessage};
32pub use spec::ForwardingSpec;
33
34use crate::ssh::tokio_client::AddressFamily;
35use anyhow::{Context, Result};
36use std::fmt;
37use std::net::{IpAddr, SocketAddr};
38
39/// Port forwarding specification types
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum ForwardingType {
42    /// Local port forwarding (-L)
43    /// Format: [bind_address:]port:host:hostport
44    Local {
45        bind_addr: IpAddr,
46        bind_port: u16,
47        remote_host: String,
48        remote_port: u16,
49    },
50    /// Remote port forwarding (-R)  
51    /// Format: [bind_address:]port:host:hostport
52    Remote {
53        bind_addr: IpAddr,
54        bind_port: u16,
55        local_host: String,
56        local_port: u16,
57    },
58    /// Dynamic port forwarding (-D)
59    /// Format: [bind_address:]port
60    Dynamic {
61        bind_addr: IpAddr,
62        bind_port: u16,
63        /// SOCKS protocol version (4 or 5)
64        socks_version: SocksVersion,
65    },
66}
67
68/// SOCKS protocol version for dynamic forwarding
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum SocksVersion {
71    V4,
72    V5,
73}
74
75/// Status of a port forwarding session
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum ForwardingStatus {
78    /// Forwarding is initializing
79    Initializing,
80    /// Forwarding is active and ready
81    Active,
82    /// Forwarding is temporarily disconnected, attempting reconnect
83    Reconnecting,
84    /// Forwarding failed and stopped
85    Failed(String),
86    /// Forwarding was stopped intentionally
87    Stopped,
88}
89
90/// Statistics for a forwarding session
91#[derive(Debug, Default, Clone)]
92pub struct ForwardingStats {
93    /// Number of active connections
94    pub active_connections: usize,
95    /// Total connections handled
96    pub total_connections: u64,
97    /// Total bytes transferred
98    pub bytes_transferred: u64,
99    /// Number of failed connections
100    pub failed_connections: u64,
101    /// Last error message if any
102    pub last_error: Option<String>,
103}
104
105/// Configuration for port forwarding behavior
106#[derive(Debug, Clone)]
107pub struct ForwardingConfig {
108    /// Maximum number of concurrent connections per forward
109    pub max_connections: usize,
110    /// Connection timeout in seconds
111    pub connect_timeout_secs: u64,
112    /// Enable automatic reconnection on failure
113    pub auto_reconnect: bool,
114    /// Maximum reconnection attempts (0 = unlimited)
115    pub max_reconnect_attempts: u32,
116    /// Initial reconnection delay in milliseconds
117    pub reconnect_delay_ms: u64,
118    /// Maximum reconnection delay in milliseconds (for exponential backoff)
119    pub max_reconnect_delay_ms: u64,
120    /// Buffer size for data transfer operations
121    pub buffer_size: usize,
122    /// Address family constraint applied to forwarding *targets*.
123    ///
124    /// The remote sshd performs the actual connect, so this only narrows which
125    /// resolved address bssh names in the `direct-tcpip` request: a
126    /// best-effort hint, not a guarantee. The *listener* side is constrained
127    /// separately, at specification parse time, by
128    /// [`parse_bind_spec_with_family`].
129    pub address_family: AddressFamily,
130}
131
132impl Default for ForwardingConfig {
133    fn default() -> Self {
134        Self {
135            max_connections: 100,
136            connect_timeout_secs: 30,
137            auto_reconnect: true,
138            max_reconnect_attempts: 10,
139            reconnect_delay_ms: 1000,
140            max_reconnect_delay_ms: 30000,
141            buffer_size: 8192,
142            address_family: AddressFamily::Any,
143        }
144    }
145}
146
147impl fmt::Display for ForwardingType {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        // `SocketAddr`'s own `Display` brackets IPv6 addresses ([::1]:8080)
150        // and leaves IPv4 addresses unbracketed (127.0.0.1:8080), so building
151        // one from `bind_addr`/`bind_port` keeps both forms unambiguous.
152        match self {
153            ForwardingType::Local {
154                bind_addr,
155                bind_port,
156                remote_host,
157                remote_port,
158            } => {
159                write!(
160                    f,
161                    "{}→{remote_host}:{remote_port}",
162                    SocketAddr::new(*bind_addr, *bind_port)
163                )
164            }
165            ForwardingType::Remote {
166                bind_addr,
167                bind_port,
168                local_host,
169                local_port,
170            } => {
171                write!(
172                    f,
173                    "{}←{local_host}:{local_port}",
174                    SocketAddr::new(*bind_addr, *bind_port)
175                )
176            }
177            ForwardingType::Dynamic {
178                bind_addr,
179                bind_port,
180                socks_version,
181            } => {
182                write!(
183                    f,
184                    "SOCKS{socks_version:?} proxy on {}",
185                    SocketAddr::new(*bind_addr, *bind_port)
186                )
187            }
188        }
189    }
190}
191
192impl fmt::Display for ForwardingStatus {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        match self {
195            ForwardingStatus::Initializing => write!(f, "initializing"),
196            ForwardingStatus::Active => write!(f, "active"),
197            ForwardingStatus::Reconnecting => write!(f, "reconnecting"),
198            ForwardingStatus::Failed(err) => write!(f, "failed: {err}"),
199            ForwardingStatus::Stopped => write!(f, "stopped"),
200        }
201    }
202}
203
204impl SocksVersion {
205    /// Parse SOCKS version from string
206    pub fn parse(s: &str) -> Result<Self> {
207        match s {
208            "4" | "v4" | "socks4" => Ok(SocksVersion::V4),
209            "5" | "v5" | "socks5" => Ok(SocksVersion::V5),
210            _ => Err(anyhow::anyhow!(
211                "Invalid SOCKS version: {s}. Expected 4 or 5"
212            )),
213        }
214    }
215}
216
217/// Parse a bind address specification with no address family constraint.
218///
219/// Equivalent to [`parse_bind_spec_with_family`] with [`AddressFamily::Any`].
220pub fn parse_bind_spec(spec: &str) -> Result<SocketAddr> {
221    parse_bind_spec_with_family(spec, AddressFamily::Any)
222}
223
224/// Parse a bind address specification, using `address_family` to pick the
225/// default when the specification does not name an address.
226///
227/// Formats supported:
228/// - `port` -> loopback:port (`127.0.0.1` by default, `::1` under `-6`)
229/// - `address:port` -> address:port (explicit address always wins)
230/// - `*:port` -> wildcard:port (`0.0.0.0` by default, `::` under `-6`)
231pub fn parse_bind_spec_with_family(
232    spec: &str,
233    address_family: AddressFamily,
234) -> Result<SocketAddr> {
235    // Handle different bind specification formats
236    if let Ok(port) = spec.parse::<u16>() {
237        // Just a port number, bind to the family's loopback address
238        return Ok(SocketAddr::new(address_family.loopback(), port));
239    }
240
241    // Check for wildcard binding
242    if let Some(port_str) = spec.strip_prefix("*:") {
243        let port = port_str
244            .parse::<u16>()
245            .with_context(|| format!("Invalid port in bind specification: {spec}"))?;
246        return Ok(SocketAddr::new(address_family.unspecified(), port));
247    }
248
249    // Parse as full socket address. An explicit bind address always wins over
250    // the address family flag, matching the decision recorded in issue #246.
251    spec.parse::<SocketAddr>()
252        .with_context(|| format!("Invalid bind specification: {spec}"))
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258    use std::net::{Ipv4Addr, Ipv6Addr};
259
260    #[test]
261    fn test_parse_bind_spec_with_forced_family() {
262        // -6 moves the implicit loopback default from 127.0.0.1 to ::1.
263        let addr = parse_bind_spec_with_family("8080", AddressFamily::V6).unwrap();
264        assert_eq!(addr.ip(), IpAddr::V6(Ipv6Addr::LOCALHOST));
265        assert_eq!(addr.port(), 8080);
266
267        // -6 moves the wildcard default from 0.0.0.0 to ::.
268        let addr = parse_bind_spec_with_family("*:8080", AddressFamily::V6).unwrap();
269        assert_eq!(addr.ip(), IpAddr::V6(Ipv6Addr::UNSPECIFIED));
270
271        // -4 keeps the historical IPv4 defaults.
272        let addr = parse_bind_spec_with_family("8080", AddressFamily::V4).unwrap();
273        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
274        let addr = parse_bind_spec_with_family("*:8080", AddressFamily::V4).unwrap();
275        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::UNSPECIFIED));
276
277        // An explicit bind address always wins over the flag.
278        let addr = parse_bind_spec_with_family("192.168.1.1:8080", AddressFamily::V6).unwrap();
279        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
280        let addr = parse_bind_spec_with_family("[::1]:8080", AddressFamily::V4).unwrap();
281        assert_eq!(addr.ip(), IpAddr::V6(Ipv6Addr::LOCALHOST));
282    }
283
284    #[test]
285    fn test_parse_bind_spec() {
286        // Test port-only specification
287        let addr = parse_bind_spec("8080").unwrap();
288        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::LOCALHOST));
289        assert_eq!(addr.port(), 8080);
290
291        // Test wildcard binding
292        let addr = parse_bind_spec("*:8080").unwrap();
293        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::UNSPECIFIED));
294        assert_eq!(addr.port(), 8080);
295
296        // Test explicit IP binding
297        let addr = parse_bind_spec("192.168.1.1:8080").unwrap();
298        assert_eq!(addr.ip(), IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
299        assert_eq!(addr.port(), 8080);
300
301        // Test IPv6
302        let addr = parse_bind_spec("[::1]:8080").unwrap();
303        assert_eq!(addr.port(), 8080);
304    }
305
306    #[test]
307    fn test_socks_version_parse() {
308        assert_eq!(SocksVersion::parse("4").unwrap(), SocksVersion::V4);
309        assert_eq!(SocksVersion::parse("v5").unwrap(), SocksVersion::V5);
310        assert_eq!(SocksVersion::parse("socks4").unwrap(), SocksVersion::V4);
311        assert!(SocksVersion::parse("invalid").is_err());
312    }
313
314    #[test]
315    fn test_forwarding_type_display() {
316        let local = ForwardingType::Local {
317            bind_addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
318            bind_port: 8080,
319            remote_host: "example.com".to_string(),
320            remote_port: 80,
321        };
322        assert_eq!(format!("{local}"), "127.0.0.1:8080→example.com:80");
323
324        let dynamic = ForwardingType::Dynamic {
325            bind_addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
326            bind_port: 1080,
327            socks_version: SocksVersion::V5,
328        };
329        assert!(format!("{dynamic}").contains("SOCKS"));
330    }
331
332    #[test]
333    fn test_forwarding_type_display_brackets_ipv6() {
334        // IPv4 stays unbracketed.
335        let local_v4 = ForwardingType::Local {
336            bind_addr: IpAddr::V4(Ipv4Addr::LOCALHOST),
337            bind_port: 8080,
338            remote_host: "example.com".to_string(),
339            remote_port: 80,
340        };
341        assert_eq!(format!("{local_v4}"), "127.0.0.1:8080→example.com:80");
342
343        // IPv6 must be bracketed so `host:port` is unambiguous.
344        let local_v6 = ForwardingType::Local {
345            bind_addr: IpAddr::V6(Ipv6Addr::LOCALHOST),
346            bind_port: 8080,
347            remote_host: "example.com".to_string(),
348            remote_port: 80,
349        };
350        assert_eq!(format!("{local_v6}"), "[::1]:8080→example.com:80");
351
352        let remote_v6 = ForwardingType::Remote {
353            bind_addr: IpAddr::V6(Ipv6Addr::LOCALHOST),
354            bind_port: 9090,
355            local_host: "localhost".to_string(),
356            local_port: 22,
357        };
358        assert_eq!(format!("{remote_v6}"), "[::1]:9090←localhost:22");
359
360        let dynamic_v6 = ForwardingType::Dynamic {
361            bind_addr: IpAddr::V6(Ipv6Addr::LOCALHOST),
362            bind_port: 1080,
363            socks_version: SocksVersion::V5,
364        };
365        assert_eq!(format!("{dynamic_v6}"), "SOCKSV5 proxy on [::1]:1080");
366    }
367}