toride_ssh_forward/
lib.rs1pub mod control;
5
6use std::collections::HashMap;
7use std::path::Path;
8
9use toride_ssh_core::SshPaths;
10use toride_ssh_core::{Error, Result};
11
12pub use control::{ControlSession, ForwardType, PortForward};
13
14pub struct ForwardService<'a> {
18 paths: &'a SshPaths,
19}
20
21impl<'a> ForwardService<'a> {
22 #[must_use]
23 pub fn new(paths: &'a SshPaths) -> Self {
24 Self { paths }
25 }
26
27 pub async fn list(&self) -> Result<Vec<(ControlSession, Vec<PortForward>)>> {
33 let sessions = self.list_sessions().await?;
34 let listings =
35 control::list_forwards_bounded(sessions.iter().map(|s| s.control_path.clone())).await;
36 Ok(sessions
37 .into_iter()
38 .zip(listings)
39 .map(|(session, result)| match result {
40 Ok(forwards) => (session, forwards),
41 Err(e) => {
42 tracing::warn!(
43 "failed to list forwards for {}: {e}",
44 session.control_path.display()
45 );
46 (session, Vec::new())
47 }
48 })
49 .collect())
50 }
51
52 pub async fn list_sessions(&self) -> Result<Vec<ControlSession>> {
57 control::list_sessions(self.paths.ssh_dir()).await
58 }
59
60 pub async fn cancel(&self, control_path: &Path, local_port: u16) -> Result<()> {
66 control::cancel_forward(control_path, local_port).await
67 }
68
69 pub async fn list_forwards(&self, control_path: &Path) -> Result<Vec<PortForward>> {
75 control::list_forwards(control_path).await
76 }
77
78 pub async fn cancel_known(&self, control_path: &Path, forward: &PortForward) -> Result<()> {
84 control::cancel_known_forward(control_path, forward).await
85 }
86
87 pub async fn exit_session(&self, control_path: &Path) -> Result<()> {
93 control::exit_session(control_path).await
94 }
95
96 pub async fn conflicting_local_ports(&self) -> Result<HashMap<u16, Vec<std::path::PathBuf>>> {
102 let sessions = self.list_sessions().await?;
103 let listings =
104 control::list_forwards_bounded(sessions.iter().map(|s| s.control_path.clone())).await;
105 let mut port_owners: HashMap<u16, Vec<std::path::PathBuf>> = HashMap::new();
106
107 for (session, result) in sessions.iter().zip(listings) {
108 match result {
109 Ok(forwards) => {
110 for fwd in forwards {
111 port_owners
112 .entry(fwd.local_port)
113 .or_default()
114 .push(session.control_path.clone());
115 }
116 }
117 Err(e) => {
118 tracing::warn!(
119 "failed to list forwards for {}: {e}",
120 session.control_path.display()
121 );
122 }
123 }
124 }
125
126 port_owners.retain(|_, owners| owners.len() > 1);
127 Ok(port_owners)
128 }
129
130 const TEST_CONNECTIVITY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(2);
131
132 pub async fn test_connectivity_with_timeout(
138 &self,
139 local_port: u16,
140 timeout: std::time::Duration,
141 ) -> Result<()> {
142 let addr = format!("127.0.0.1:{local_port}");
143
144 tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr))
145 .await
146 .map_err(|_| {
147 Error::ForwardFailed(format!(
148 "connection to {addr} timed out after {} seconds",
149 timeout.as_secs()
150 ))
151 })?
152 .map_err(|e| Error::ForwardFailed(format!("cannot connect to {addr}: {e}")))?;
153
154 tracing::debug!("successfully connected to forwarded port {local_port}");
155 Ok(())
156 }
157
158 pub async fn test_connectivity(&self, local_port: u16) -> Result<()> {
164 self.test_connectivity_with_timeout(local_port, Self::TEST_CONNECTIVITY_TIMEOUT)
165 .await
166 }
167}