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 std::path::Path;
177 use tokio::io::{AsyncReadExt, AsyncWriteExt};
178 use tokio::sync::Mutex;
179
180 struct ClienteFakeTunel {
181 canal: Mutex<Option<tokio::io::DuplexStream>>,
182 falhar_ao_abrir: bool,
183 }
184
185 impl ClienteFakeTunel {
186 fn novo(canal: tokio::io::DuplexStream) -> Self {
187 Self {
188 canal: Mutex::new(Some(canal)),
189 falhar_ao_abrir: false,
190 }
191 }
192
193 fn falhando() -> Self {
194 Self {
195 canal: Mutex::new(None),
196 falhar_ao_abrir: true,
197 }
198 }
199 }
200
201 #[async_trait]
202 impl ClienteSshTrait for ClienteFakeTunel {
203 async fn conectar(_cfg: ConfiguracaoConexao) -> Result<Box<Self>, ErroSshCli> {
204 Err(ErroSshCli::ConexaoFalhou(
205 "não implementado em teste".to_string(),
206 ))
207 }
208
209 async fn executar_comando(
210 &mut self,
211 _cmd: &str,
212 _max_chars: usize,
213 ) -> Result<SaidaExecucao, ErroSshCli> {
214 Err(ErroSshCli::CanalFalhou(
215 "não implementado em teste".to_string(),
216 ))
217 }
218
219 async fn upload(
220 &mut self,
221 _local: &Path,
222 _remote: &Path,
223 ) -> Result<TransferenciaResultado, ErroSshCli> {
224 Err(ErroSshCli::CanalFalhou(
225 "não implementado em teste".to_string(),
226 ))
227 }
228
229 async fn download(
230 &mut self,
231 _remote: &Path,
232 _local: &Path,
233 ) -> Result<TransferenciaResultado, ErroSshCli> {
234 Err(ErroSshCli::CanalFalhou(
235 "não implementado em teste".to_string(),
236 ))
237 }
238
239 async fn abrir_canal_tunel(
240 &self,
241 _host_remoto: &str,
242 _porta_remota: u16,
243 _endereco_origem: &str,
244 _porta_origem: u16,
245 ) -> Result<Box<dyn CanalTunel>, ErroSshCli> {
246 if self.falhar_ao_abrir {
247 return Err(ErroSshCli::CanalFalhou("falha forçada".to_string()));
248 }
249
250 let mut guard = self.canal.lock().await;
251 let canal = guard
252 .take()
253 .ok_or_else(|| ErroSshCli::CanalFalhou("canal já consumido".to_string()))?;
254 Ok(Box::new(canal))
255 }
256
257 async fn desconectar(&self) -> Result<(), ErroSshCli> {
258 Ok(())
259 }
260 }
261
262 #[test]
263 fn tunnel_modulo_compilou() {
264 let _ = std::file!();
266 }
267
268 #[tokio::test]
269 async fn redireciona_dados_nos_dois_sentidos() {
270 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
271 .await
272 .expect("listener local");
273 let endereco = listener.local_addr().expect("local addr");
274
275 let cliente_lado_local = tokio::net::TcpStream::connect(endereco)
276 .await
277 .expect("conecta no listener");
278 let (soquete_aceito, origem) = listener.accept().await.expect("accept local");
279
280 let (canal_ssh, mut lado_remoto) = tokio::io::duplex(4096);
281 let cliente_fake = ClienteFakeTunel::novo(canal_ssh);
282
283 let tarefa = tokio::spawn(async move {
284 redirecionar_conexao(
285 soquete_aceito,
286 "db-interna",
287 5432,
288 "vps-teste",
289 origem,
290 &cliente_fake,
291 )
292 .await
293 });
294
295 let mut cliente_lado_local = cliente_lado_local;
296 cliente_lado_local
297 .write_all(b"ping")
298 .await
299 .expect("envia ping local");
300
301 let mut buf = [0_u8; 4];
302 lado_remoto
303 .read_exact(&mut buf)
304 .await
305 .expect("le ping no canal remoto");
306 assert_eq!(&buf, b"ping");
307
308 lado_remoto
309 .write_all(b"pong")
310 .await
311 .expect("escreve pong remoto");
312
313 let mut retorno = [0_u8; 4];
314 cliente_lado_local
315 .read_exact(&mut retorno)
316 .await
317 .expect("le pong no cliente local");
318 assert_eq!(&retorno, b"pong");
319
320 cliente_lado_local.shutdown().await.expect("shutdown local");
321 lado_remoto.shutdown().await.expect("shutdown remoto");
322
323 let resultado = tarefa.await.expect("join task");
324 assert!(resultado.is_ok());
325 }
326
327 #[tokio::test]
328 async fn redirecionamento_retorna_erro_quando_falha_abrir_canal_ssh() {
329 let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
330 .await
331 .expect("listener local");
332 let endereco = listener.local_addr().expect("local addr");
333
334 let _cliente_lado_local = tokio::net::TcpStream::connect(endereco)
335 .await
336 .expect("conecta no listener");
337 let (soquete_aceito, origem) = listener.accept().await.expect("accept local");
338
339 let cliente_fake = ClienteFakeTunel::falhando();
340
341 let resultado = redirecionar_conexao(
342 soquete_aceito,
343 "db-interna",
344 5432,
345 "vps-teste",
346 origem,
347 &cliente_fake,
348 )
349 .await;
350
351 assert!(resultado.is_err());
352 }
353
354 #[tokio::test]
355 async fn executar_tunnel_with_client_inicia_listener_e_processa_conexao() {
356 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("porta livre");
357 let porta_livre = listener.local_addr().expect("addr").port();
358 drop(listener);
359
360 let (canal_ssh, mut lado_remoto) = tokio::io::duplex(4096);
361 let cliente_fake = Box::new(ClienteFakeTunel::novo(canal_ssh));
362
363 let tarefa_tunel = tokio::spawn(async move {
364 super::executar_tunnel_with_client(
365 "vps-teste",
366 porta_livre,
367 "db-interna",
368 5432,
369 cliente_fake,
370 )
371 .await
372 });
373
374 let mut cliente_local = loop {
375 match tokio::net::TcpStream::connect(("127.0.0.1", porta_livre)).await {
376 Ok(stream) => break stream,
377 Err(_) => tokio::time::sleep(std::time::Duration::from_millis(10)).await,
378 }
379 };
380
381 cliente_local
382 .write_all(b"ok")
383 .await
384 .expect("envia bytes locais");
385
386 let mut recebido = [0_u8; 2];
387 lado_remoto
388 .read_exact(&mut recebido)
389 .await
390 .expect("lê bytes no canal remoto");
391 assert_eq!(&recebido, b"ok");
392
393 tarefa_tunel.abort();
394 }
395}