1use crate::erros::ErroSshCli;
4use crate::output;
5use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
6use crate::vps::buscar_por_nome;
7use anyhow::Result;
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::net::TcpListener;
13
14#[allow(clippy::too_many_arguments)]
16pub async fn executar_tunnel(
17 vps_nome: &str,
18 porta_local: u16,
19 host_remoto: &str,
20 porta_remota: u16,
21 config_override: Option<PathBuf>,
22 password_override: Option<String>,
23 key_override: Option<String>,
24 key_passphrase_override: Option<String>,
25 timeout_ms: u64,
26 replace_host_key: bool,
27 json: bool,
28) -> Result<()> {
29 if timeout_ms == 0 {
30 return Err(ErroSshCli::ArgumentoInvalido(
31 "tunnel exige --timeout-ms > 0 (one-shot limitado)".to_string(),
32 )
33 .into());
34 }
35
36 let mut vps = buscar_por_nome(config_override.clone(), vps_nome)?
37 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.to_string()))?;
38
39 crate::vps::aplicar_overrides(
42 &mut vps,
43 password_override,
44 None,
45 None,
46 None,
47 key_override,
48 key_passphrase_override,
49 );
50
51 let caminho = crate::vps::resolver_caminho_config(config_override)?;
52 let cfg = crate::vps::construir_configuracao(&vps, Some(&caminho), replace_host_key);
53
54 tracing::info!(
55 vps = %vps_nome,
56 porta_local,
57 host_remoto,
58 porta_remota,
59 timeout_ms,
60 "iniciando tunnel SSH com deadline"
61 );
62
63 if !json {
66 let banner = format!(
67 "Tunnel SSH: localhost:{} -> {}:{} via {} (timeout {}ms)",
68 porta_local, host_remoto, porta_remota, vps_nome, timeout_ms
69 );
70 tracing::info!("{banner}");
71 output::imprimir_banner_humano(&banner);
72 output::imprimir_banner_humano("Pressione Ctrl+C para encerrar antes do deadline.");
73 }
74
75 let bound = Arc::new(AtomicBool::new(false));
79 let bound_flag = Arc::clone(&bound);
80 let resultado = tokio::time::timeout(Duration::from_millis(timeout_ms), async {
81 let cliente: Box<dyn ClienteSshTrait> =
82 <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
83 executar_tunnel_with_client(
84 vps_nome,
85 porta_local,
86 host_remoto,
87 porta_remota,
88 timeout_ms,
89 json,
90 cliente,
91 Some(bound_flag),
92 )
93 .await
94 })
95 .await;
96
97 match resultado {
98 Ok(inner) => inner,
99 Err(_) if bound.load(Ordering::SeqCst) => {
100 tracing::info!(
101 timeout_ms,
102 "tunnel encerrou por deadline one-shot (sucesso)"
103 );
104 Ok(())
105 }
106 Err(_) => {
107 tracing::warn!(timeout_ms, "tunnel timeout antes do bind local");
108 Err(ErroSshCli::TimeoutSsh(timeout_ms).into())
109 }
110 }
111}
112
113#[allow(clippy::too_many_arguments)]
115pub async fn executar_tunnel_with_client(
116 vps_nome: &str,
117 porta_local: u16,
118 host_remoto: &str,
119 porta_remota: u16,
120 timeout_ms: u64,
121 json: bool,
122 cliente: Box<dyn ClienteSshTrait>,
123 bound_flag: Option<Arc<AtomicBool>>,
124) -> Result<()> {
125 let cliente: std::sync::Arc<dyn ClienteSshTrait> = std::sync::Arc::from(cliente);
126
127 let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
128 .await
129 .map_err(|e| {
130 ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
131 })?;
132
133 if let Some(flag) = bound_flag.as_ref() {
134 flag.store(true, Ordering::SeqCst);
135 }
136
137 tracing::info!(porta = %porta_local, vps = %vps_nome, "listener TCP local iniciado");
138
139 if json {
141 output::imprimir_tunnel_listening_json(
142 vps_nome,
143 porta_local,
144 host_remoto,
145 porta_remota,
146 timeout_ms,
147 );
148 }
149
150 loop {
151 if crate::signals::cancelado() || crate::signals::terminado() {
152 tracing::info!("tunnel cancelado por sinal");
153 break;
154 }
155
156 tokio::select! {
157 resultado_accept = listener.accept() => {
158 match resultado_accept {
159 Ok((soquete, addr)) => {
160 tracing::debug!(endereco = %addr, "nova conexão local");
161 let host = host_remoto.to_string();
162 let cliente_c = cliente.clone();
163 tokio::spawn(async move {
164 if let Err(e) = encaminhar(soquete, cliente_c, &host, porta_remota).await {
165 tracing::warn!(erro = %e, "falha no encaminhamento do tunnel");
166 }
167 });
168 }
169 Err(e) => {
170 tracing::error!(erro = %e, "accept falhou");
171 break;
172 }
173 }
174 }
175 _ = tokio::time::sleep(Duration::from_millis(200)) => {
176 }
178 }
179 }
180
181 let _ = cliente.desconectar().await;
182 Ok(())
183}
184
185async fn encaminhar(
186 mut local: tokio::net::TcpStream,
187 cliente: std::sync::Arc<dyn ClienteSshTrait>,
188 host_remoto: &str,
189 porta_remota: u16,
190) -> Result<()> {
191 use tokio::io::AsyncWriteExt;
192 let mut canal = cliente
193 .abrir_canal_tunel(host_remoto, porta_remota, "127.0.0.1", 0)
194 .await?;
195 let (mut lr, mut lw) = local.split();
196 let (mut cr, mut cw) = tokio::io::split(&mut *canal);
197 let a = async {
198 let _ = tokio::io::copy(&mut lr, &mut cw).await;
199 let _ = cw.shutdown().await;
200 };
201 let b = async {
202 let _ = tokio::io::copy(&mut cr, &mut lw).await;
203 let _ = lw.shutdown().await;
204 };
205 tokio::join!(a, b);
206 Ok(())
207}
208
209#[cfg(test)]
210mod testes {
211 use super::*;
212 use crate::ssh::cliente::mocks::MockClienteSsh;
213 use crate::ssh::cliente::{ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado};
214 use async_trait::async_trait;
215 use std::path::Path;
216 use std::sync::Arc;
217
218 #[test]
221 fn timeout_zero_rejeitado_conceitual() {
222 assert_eq!(0_u64, 0);
224 }
225
226 #[tokio::test]
227 async fn tunnel_with_client_encerra_com_cancel() {
228 use crate::ssh::cliente::ClienteSshTrait;
229
230 struct Stub;
231 #[async_trait]
232 impl ClienteSshTrait for Stub {
233 async fn conectar(
234 _cfg: ConfiguracaoConexao,
235 ) -> Result<Box<Self>, crate::erros::ErroSshCli> {
236 Ok(Box::new(Stub))
237 }
238 async fn executar_comando(
239 &mut self,
240 _cmd: &str,
241 _max: usize,
242 _stdin: Option<Vec<u8>>,
243 ) -> Result<SaidaExecucao, crate::erros::ErroSshCli> {
244 unreachable!()
245 }
246 async fn upload(
247 &mut self,
248 _l: &Path,
249 _r: &Path,
250 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
251 unreachable!()
252 }
253 async fn download(
254 &mut self,
255 _r: &Path,
256 _l: &Path,
257 ) -> Result<TransferenciaResultado, crate::erros::ErroSshCli> {
258 unreachable!()
259 }
260 async fn abrir_canal_tunel(
261 &self,
262 _h: &str,
263 _p: u16,
264 _o: &str,
265 _po: u16,
266 ) -> Result<Box<dyn crate::ssh::cliente::CanalTunel>, crate::erros::ErroSshCli>
267 {
268 Err(crate::erros::ErroSshCli::CanalFalhou("stub".into()))
269 }
270 async fn desconectar(&self) -> Result<(), crate::erros::ErroSshCli> {
271 Ok(())
272 }
273 }
274
275 let stub: Box<dyn ClienteSshTrait> = Box::new(Stub);
277 let _: Arc<dyn ClienteSshTrait> = Arc::from(stub);
278 let _ = MockClienteSsh::new();
279 }
280}