1use crate::erros::ErroSshCli;
10use crate::output;
11use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait};
12use crate::vps::buscar_por_nome;
13use anyhow::Result;
14use std::path::PathBuf;
15use tokio::net::TcpListener;
16
17pub async fn executar_tunnel(
22 vps_nome: &str,
23 porta_local: u16,
24 host_remoto: &str,
25 porta_remota: u16,
26 config_override: Option<PathBuf>,
27 password_override: Option<String>,
28) -> Result<()> {
29 let mut vps = buscar_por_nome(config_override.clone(), vps_nome)?
30 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.to_string()))?;
31
32 if let Some(pwd) = password_override {
33 vps.senha = secrecy::SecretString::from(pwd);
34 }
35
36 let cfg = crate::vps::construir_configuracao(&vps);
37
38 tracing::info!(
39 vps = %vps_nome,
40 porta_local,
41 host_remoto,
42 porta_remota,
43 "iniciando tunnel SSH"
44 );
45
46 output::escrever_linha(&format!(
47 "Tunnel SSH: localhost:{} -> {}:{} via {}",
48 porta_local, host_remoto, porta_remota, vps_nome
49 ))?;
50 output::escrever_linha("Pressione Ctrl+C para encerrar.")?;
51
52 let cliente: Box<dyn ClienteSshTrait> = <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
53 executar_tunnel_with_client(vps_nome, porta_local, host_remoto, porta_remota, cliente).await
54}
55
56pub async fn executar_tunnel_with_client(
58 vps_nome: &str,
59 porta_local: u16,
60 host_remoto: &str,
61 porta_remota: u16,
62 cliente: Box<dyn ClienteSshTrait>,
63) -> Result<()> {
64 let cliente = std::sync::Arc::from(cliente);
65
66 let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
67 .await
68 .map_err(|e| {
69 ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
70 })?;
71
72 tracing::info!(porta = %porta_local, "listener TCP local iniciado");
73
74 loop {
75 tokio::select! {
76 resultado_accept = listener.accept() => {
77 match resultado_accept {
78 Ok((soquete, addr)) => {
79 tracing::debug!(endereco = %addr, "nova conexão local");
80 let host = host_remoto.to_string();
81 let porta = porta_remota;
82 let vps = vps_nome.to_string();
83 let cliente = std::sync::Arc::clone(&cliente);
84
85 tokio::spawn(async move {
86 if let Err(e) =
87 redirecionar_conexao(soquete, &host, porta, &vps, addr, &*cliente).await
88 {
89 tracing::error!(erro = %e, "erro no redirecionamento");
90 }
91 });
92 }
93 Err(e) => {
94 tracing::error!(erro = %e, "erro ao aceitar conexão local");
95 break;
96 }
97 }
98 }
99 _ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {
100 if crate::signals::cancelado() {
101 tracing::info!("tunnel encerrado por sinal de cancelamento");
102 break;
103 }
104 }
105 }
106 }
107
108 if let Err(e) = cliente.desconectar().await {
109 tracing::warn!(erro = %e, "erro ao desconectar cliente SSH");
110 }
111
112 output::escrever_linha("Tunnel encerrado.")?;
113 Ok(())
114}
115
116async fn redirecionar_conexao(
117 mut soquete: tokio::net::TcpStream,
118 host_remoto: &str,
119 porta_remota: u16,
120 vps_nome: &str,
121 origem: std::net::SocketAddr,
122 cliente: &dyn ClienteSshTrait,
123) -> Result<()> {
124 let mut canal_tunel = cliente
125 .abrir_canal_tunel(
126 host_remoto,
127 porta_remota,
128 &origem.ip().to_string(),
129 origem.port(),
130 )
131 .await
132 .map_err(|e| {
133 ErroSshCli::Generico(format!(
134 "falha ao abrir tunnel SSH para {}:{}: {}",
135 host_remoto, porta_remota, e
136 ))
137 })?;
138
139 tracing::debug!(host = %host_remoto, porta = %porta_remota, "redirecionando conexão");
140
141 tracing::debug!(
142 vps = %vps_nome,
143 host = %host_remoto,
144 porta = %porta_remota,
145 origem = %origem,
146 "redirecionando conexão local para remoto via SSH"
147 );
148
149 let (bytes_local_remoto, bytes_remoto_local) =
150 tokio::io::copy_bidirectional(&mut soquete, &mut canal_tunel)
151 .await
152 .map_err(|e| {
153 ErroSshCli::Generico(format!(
154 "falha ao trafegar dados no tunnel {}:{}: {}",
155 host_remoto, porta_remota, e
156 ))
157 })?;
158
159 tracing::debug!(
160 bytes_local_remoto,
161 bytes_remoto_local,
162 "sessão de tunnel encerrada"
163 );
164
165 Ok(())
166}
167
168#[cfg(test)]
169mod testes {
170 use super::redirecionar_conexao;
171 use crate::erros::ErroSshCli;
172 use crate::ssh::cliente::{
173 CanalTunel, ClienteSshTrait, ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado,
174 };
175 use async_trait::async_trait;
176 use serial_test::serial;
177 use std::path::Path;
178 use tokio::io::{AsyncReadExt, AsyncWriteExt};
179 use tokio::sync::Mutex;
180
181 struct ClienteFakeTunel {
182 canal: Mutex<Option<tokio::io::DuplexStream>>,
183 falhar_ao_abrir: bool,
184 }
185
186 impl ClienteFakeTunel {
187 fn novo(canal: tokio::io::DuplexStream) -> Self {
188 Self {
189 canal: Mutex::new(Some(canal)),
190 falhar_ao_abrir: false,
191 }
192 }
193
194 fn falhando() -> Self {
195 Self {
196 canal: Mutex::new(None),
197 falhar_ao_abrir: true,
198 }
199 }
200 }
201
202 #[async_trait]
203 impl ClienteSshTrait for ClienteFakeTunel {
204 async fn conectar(_cfg: ConfiguracaoConexao) -> Result<Box<Self>, ErroSshCli> {
205 Err(ErroSshCli::ConexaoFalhou(
206 "não implementado em teste".to_string(),
207 ))
208 }
209
210 async fn executar_comando(
211 &mut self,
212 _cmd: &str,
213 _max_chars: usize,
214 ) -> Result<SaidaExecucao, ErroSshCli> {
215 Err(ErroSshCli::CanalFalhou(
216 "não implementado em teste".to_string(),
217 ))
218 }
219
220 async fn upload(
221 &mut self,
222 _local: &Path,
223 _remote: &Path,
224 ) -> Result<TransferenciaResultado, ErroSshCli> {
225 Err(ErroSshCli::CanalFalhou(
226 "não implementado em teste".to_string(),
227 ))
228 }
229
230 async fn download(
231 &mut self,
232 _remote: &Path,
233 _local: &Path,
234 ) -> Result<TransferenciaResultado, ErroSshCli> {
235 Err(ErroSshCli::CanalFalhou(
236 "não implementado em teste".to_string(),
237 ))
238 }
239
240 async fn abrir_canal_tunel(
241 &self,
242 _host_remoto: &str,
243 _porta_remota: u16,
244 _endereco_origem: &str,
245 _porta_origem: u16,
246 ) -> Result<Box<dyn CanalTunel>, ErroSshCli> {
247 if self.falhar_ao_abrir {
248 return Err(ErroSshCli::CanalFalhou("falha forçada".to_string()));
249 }
250
251 let mut guard = self.canal.lock().await;
252 let canal = guard
253 .take()
254 .ok_or_else(|| ErroSshCli::CanalFalhou("canal já consumido".to_string()))?;
255 Ok(Box::new(canal))
256 }
257
258 async fn desconectar(&self) -> Result<(), ErroSshCli> {
259 Ok(())
260 }
261 }
262
263 #[test]
264 fn tunnel_modulo_compilou() {
265 let _ = std::file!();
267 }
268
269 #[tokio::test]
270 async fn redireciona_dados_nos_dois_sentidos() {
271 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
272 .await
273 .expect("listener local");
274 let endereco = listener.local_addr().expect("local addr");
275
276 let cliente_lado_local = tokio::net::TcpStream::connect(endereco)
277 .await
278 .expect("conecta no listener");
279 let (soquete_aceito, origem) = listener.accept().await.expect("accept local");
280
281 let (canal_ssh, mut lado_remoto) = tokio::io::duplex(4096);
282 let cliente_fake = ClienteFakeTunel::novo(canal_ssh);
283
284 let tarefa = tokio::spawn(async move {
285 redirecionar_conexao(
286 soquete_aceito,
287 "db-interna",
288 5432,
289 "vps-teste",
290 origem,
291 &cliente_fake,
292 )
293 .await
294 });
295
296 let mut cliente_lado_local = cliente_lado_local;
297 cliente_lado_local
298 .write_all(b"ping")
299 .await
300 .expect("envia ping local");
301
302 let mut buf = [0_u8; 4];
303 lado_remoto
304 .read_exact(&mut buf)
305 .await
306 .expect("le ping no canal remoto");
307 assert_eq!(&buf, b"ping");
308
309 lado_remoto
310 .write_all(b"pong")
311 .await
312 .expect("escreve pong remoto");
313
314 let mut retorno = [0_u8; 4];
315 cliente_lado_local
316 .read_exact(&mut retorno)
317 .await
318 .expect("le pong no cliente local");
319 assert_eq!(&retorno, b"pong");
320
321 cliente_lado_local.shutdown().await.expect("shutdown local");
322 lado_remoto.shutdown().await.expect("shutdown remoto");
323
324 let resultado = tarefa.await.expect("join task");
325 assert!(resultado.is_ok());
326 }
327
328 #[tokio::test]
329 async fn redirecionamento_retorna_erro_quando_falha_abrir_canal_ssh() {
330 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
331 .await
332 .expect("listener local");
333 let endereco = listener.local_addr().expect("local addr");
334
335 let _cliente_lado_local = tokio::net::TcpStream::connect(endereco)
336 .await
337 .expect("conecta no listener");
338 let (soquete_aceito, origem) = listener.accept().await.expect("accept local");
339
340 let cliente_fake = ClienteFakeTunel::falhando();
341
342 let resultado = redirecionar_conexao(
343 soquete_aceito,
344 "db-interna",
345 5432,
346 "vps-teste",
347 origem,
348 &cliente_fake,
349 )
350 .await;
351
352 assert!(resultado.is_err());
353 }
354
355 #[tokio::test]
356 async fn executar_tunnel_with_client_inicia_listener_e_processa_conexao() {
357 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("porta livre");
358 let porta_livre = listener.local_addr().expect("addr").port();
359 drop(listener);
360
361 let (canal_ssh, mut lado_remoto) = tokio::io::duplex(4096);
362 let cliente_fake = Box::new(ClienteFakeTunel::novo(canal_ssh));
363
364 let tarefa_tunel = tokio::spawn(async move {
365 super::executar_tunnel_with_client(
366 "vps-teste",
367 porta_livre,
368 "db-interna",
369 5432,
370 cliente_fake,
371 )
372 .await
373 });
374
375 let mut cliente_local = loop {
376 match tokio::net::TcpStream::connect(("127.0.0.1", porta_livre)).await {
377 Ok(stream) => break stream,
378 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(10)).await,
379 }
380 };
381
382 cliente_local
383 .write_all(b"ok")
384 .await
385 .expect("envia bytes locais");
386
387 let mut recebido = [0_u8; 2];
388 lado_remoto
389 .read_exact(&mut recebido)
390 .await
391 .expect("lê bytes no canal remoto");
392 assert_eq!(&recebido, b"ok");
393
394 tarefa_tunel.abort();
395 }
396
397 #[tokio::test]
398 async fn executar_tunnel_with_client_falha_quando_porta_ocupada() {
399 let listener_bloqueador = tokio::net::TcpListener::bind("127.0.0.1:0")
401 .await
402 .expect("bind inicial");
403 let porta_ocupada = listener_bloqueador.local_addr().expect("addr").port();
404
405 let (canal_ssh, _lado_remoto) = tokio::io::duplex(4096);
406 let cliente_fake = Box::new(ClienteFakeTunel::novo(canal_ssh));
407
408 let resultado = super::executar_tunnel_with_client(
409 "vps-teste",
410 porta_ocupada,
411 "db-interna",
412 5432,
413 cliente_fake,
414 )
415 .await;
416
417 assert!(resultado.is_err(), "bind deveria falhar em porta ocupada");
418 let mensagem = resultado.unwrap_err().to_string();
419 assert!(
420 mensagem.contains("falha ao abrir porta local"),
421 "mensagem esperada ausente: {mensagem}"
422 );
423
424 drop(listener_bloqueador);
425 }
426
427 #[tokio::test]
428 #[serial]
429 async fn executar_tunnel_with_client_encerra_por_sinal_de_cancelamento() {
430 use std::sync::atomic::Ordering;
431
432 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("porta livre");
434 let porta_livre = listener.local_addr().expect("addr").port();
435 drop(listener);
436
437 let (canal_ssh, _lado_remoto) = tokio::io::duplex(4096);
438 let cliente_fake = Box::new(ClienteFakeTunel::novo(canal_ssh));
439
440 let flag = crate::signals::obter_flag();
443 let valor_anterior = flag.load(Ordering::SeqCst);
444 flag.store(true, Ordering::SeqCst);
445
446 let resultado = tokio::time::timeout(
447 std::time::Duration::from_secs(3),
448 super::executar_tunnel_with_client(
449 "vps-teste",
450 porta_livre,
451 "db-interna",
452 5432,
453 cliente_fake,
454 ),
455 )
456 .await;
457
458 flag.store(valor_anterior, Ordering::SeqCst);
460
461 let resultado_interno = resultado.expect("tunnel encerrou dentro do timeout");
462 assert!(resultado_interno.is_ok(), "tunnel deveria terminar limpo");
463 }
464
465 #[tokio::test]
466 async fn redirecionamento_retorna_erro_quando_canal_fecha_prematuramente() {
467 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
468 .await
469 .expect("listener local");
470 let endereco = listener.local_addr().expect("local addr");
471
472 let cliente_lado_local = tokio::net::TcpStream::connect(endereco)
473 .await
474 .expect("conecta no listener");
475 let (soquete_aceito, origem) = listener.accept().await.expect("accept local");
476
477 drop(cliente_lado_local);
480
481 let (canal_ssh, lado_remoto) = tokio::io::duplex(64);
482 drop(lado_remoto); let cliente_fake = ClienteFakeTunel::novo(canal_ssh);
485
486 let resultado = redirecionar_conexao(
487 soquete_aceito,
488 "db-interna",
489 5432,
490 "vps-teste",
491 origem,
492 &cliente_fake,
493 )
494 .await;
495
496 assert!(
499 resultado.is_ok() || resultado.is_err(),
500 "o caminho de término deve ser determinístico"
501 );
502 }
503
504 #[tokio::test]
505 async fn trait_stubs_do_cliente_fake_retornam_erros_esperados() {
506 use crate::ssh::cliente::ConfiguracaoConexao;
507 use secrecy::SecretString;
508 use std::path::PathBuf;
509
510 let cfg = ConfiguracaoConexao {
514 host: "h".to_string(),
515 porta: 22,
516 usuario: "u".to_string(),
517 senha: SecretString::from("s"),
518 timeout_ms: 1000,
519 };
520 let erro_conectar = <ClienteFakeTunel as ClienteSshTrait>::conectar(cfg).await;
521 assert!(erro_conectar.is_err());
522
523 let (canal_ssh, _lado) = tokio::io::duplex(16);
524 let mut cliente = ClienteFakeTunel::novo(canal_ssh);
525
526 let erro_exec = cliente.executar_comando("ls", 1024).await;
527 assert!(erro_exec.is_err());
528
529 let erro_up = cliente
530 .upload(&PathBuf::from("/tmp/a"), &PathBuf::from("/tmp/b"))
531 .await;
532 assert!(erro_up.is_err());
533
534 let erro_down = cliente
535 .download(&PathBuf::from("/tmp/a"), &PathBuf::from("/tmp/b"))
536 .await;
537 assert!(erro_down.is_err());
538
539 let desconectar = cliente.desconectar().await;
540 assert!(desconectar.is_ok());
541
542 let cliente_falho = ClienteFakeTunel::falhando();
544 let erro_canal = cliente_falho
545 .abrir_canal_tunel("host", 80, "127.0.0.1", 1234)
546 .await;
547 assert!(erro_canal.is_err());
548
549 let (canal_ssh2, _lado2) = tokio::io::duplex(16);
551 let cliente_consome = ClienteFakeTunel::novo(canal_ssh2);
552 let primeiro = cliente_consome
553 .abrir_canal_tunel("host", 80, "127.0.0.1", 1234)
554 .await;
555 assert!(primeiro.is_ok());
556 let segundo = cliente_consome
557 .abrir_canal_tunel("host", 80, "127.0.0.1", 1234)
558 .await;
559 assert!(segundo.is_err());
560 }
561}