1use crate::erros::ErroSshCli;
10use crate::output;
11use crate::ssh::cliente::{ClienteSsh, ClienteSshTrait, ConfiguracaoConexao};
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) -> Result<()> {
28 let vps = buscar_por_nome(config_override.clone(), vps_nome)?
29 .ok_or_else(|| ErroSshCli::VpsNaoEncontrada(vps_nome.to_string()))?;
30
31 let cfg = ConfiguracaoConexao {
32 host: vps.host.clone(),
33 porta: vps.porta,
34 usuario: vps.usuario.clone(),
35 senha: vps.senha.clone(),
36 timeout_ms: vps.timeout_ms,
37 };
38
39 tracing::info!(
40 vps = %vps_nome,
41 porta_local,
42 host_remoto,
43 porta_remota,
44 "iniciando tunnel SSH"
45 );
46
47 output::escrever_linha(&format!(
48 "Tunnel SSH: localhost:{} -> {}:{} via {}",
49 porta_local, host_remoto, porta_remota, vps_nome
50 ))?;
51 output::escrever_linha("Pressione Ctrl+C para encerrar.")?;
52
53 let cliente: Box<dyn ClienteSshTrait> = <ClienteSsh as ClienteSshTrait>::conectar(cfg).await?;
54 executar_tunnel_with_client(vps_nome, porta_local, host_remoto, porta_remota, cliente).await
55}
56
57pub async fn executar_tunnel_with_client(
59 vps_nome: &str,
60 porta_local: u16,
61 host_remoto: &str,
62 porta_remota: u16,
63 cliente: Box<dyn ClienteSshTrait>,
64) -> Result<()> {
65 let cliente = std::sync::Arc::from(cliente);
66
67 let listener = TcpListener::bind(format!("127.0.0.1:{porta_local}"))
68 .await
69 .map_err(|e| {
70 ErroSshCli::Generico(format!("falha ao abrir porta local {}: {}", porta_local, e))
71 })?;
72
73 tracing::info!(porta = %porta_local, "listener TCP local iniciado");
74
75 loop {
76 tokio::select! {
77 resultado_accept = listener.accept() => {
78 match resultado_accept {
79 Ok((soquete, addr)) => {
80 tracing::debug!(endereco = %addr, "nova conexão local");
81 let host = host_remoto.to_string();
82 let porta = porta_remota;
83 let vps = vps_nome.to_string();
84 let cliente = std::sync::Arc::clone(&cliente);
85
86 tokio::spawn(async move {
87 if let Err(e) =
88 redirecionar_conexao(soquete, &host, porta, &vps, addr, &*cliente).await
89 {
90 tracing::error!(erro = %e, "erro no redirecionamento");
91 }
92 });
93 }
94 Err(e) => {
95 tracing::error!(erro = %e, "erro ao aceitar conexão local");
96 break;
97 }
98 }
99 }
100 _ = tokio::time::sleep(std::time::Duration::from_millis(200)) => {
101 if crate::signals::cancelado() {
102 tracing::info!("tunnel encerrado por sinal de cancelamento");
103 break;
104 }
105 }
106 }
107 }
108
109 if let Err(e) = cliente.desconectar().await {
110 tracing::warn!(erro = %e, "erro ao desconectar cliente SSH");
111 }
112
113 output::escrever_linha("Tunnel encerrado.")?;
114 Ok(())
115}
116
117async fn redirecionar_conexao(
118 mut soquete: tokio::net::TcpStream,
119 host_remoto: &str,
120 porta_remota: u16,
121 vps_nome: &str,
122 origem: std::net::SocketAddr,
123 cliente: &dyn ClienteSshTrait,
124) -> Result<()> {
125 let mut canal_tunel = cliente
126 .abrir_canal_tunel(
127 host_remoto,
128 porta_remota,
129 &origem.ip().to_string(),
130 origem.port(),
131 )
132 .await
133 .map_err(|e| {
134 ErroSshCli::Generico(format!(
135 "falha ao abrir tunnel SSH para {}:{}: {}",
136 host_remoto, porta_remota, e
137 ))
138 })?;
139
140 tracing::debug!(host = %host_remoto, porta = %porta_remota, "redirecionando conexão");
141
142 tracing::debug!(
143 vps = %vps_nome,
144 host = %host_remoto,
145 porta = %porta_remota,
146 origem = %origem,
147 "redirecionando conexão local para remoto via SSH"
148 );
149
150 let (bytes_local_remoto, bytes_remoto_local) =
151 tokio::io::copy_bidirectional(&mut soquete, &mut canal_tunel)
152 .await
153 .map_err(|e| {
154 ErroSshCli::Generico(format!(
155 "falha ao trafegar dados no tunnel {}:{}: {}",
156 host_remoto, porta_remota, e
157 ))
158 })?;
159
160 tracing::debug!(
161 bytes_local_remoto,
162 bytes_remoto_local,
163 "sessão de tunnel encerrada"
164 );
165
166 Ok(())
167}
168
169#[cfg(test)]
170mod testes {
171 use super::redirecionar_conexao;
172 use crate::erros::ErroSshCli;
173 use crate::ssh::cliente::{
174 CanalTunel, ClienteSshTrait, ConfiguracaoConexao, SaidaExecucao, TransferenciaResultado,
175 };
176 use async_trait::async_trait;
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}