Tunnels are now always stored in the handler

If tunnel.domain is set that means the tunnel is actually open.
This should make it possible to retry a failed tunnel in the future.
This commit is contained in:
Dreaded_X 2025-04-11 17:03:39 +02:00
parent da9dc7700c
commit 4fa885843f
Signed by: Dreaded_X
GPG Key ID: FA5F485356B0D2D4
6 changed files with 88 additions and 73 deletions

1
Cargo.lock generated
View File

@ -3221,7 +3221,6 @@ dependencies = [
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-util", "hyper-util",
"indexmap",
"rand 0.8.5", "rand 0.8.5",
"ratatui", "ratatui",
"reqwest", "reqwest",

View File

@ -15,7 +15,6 @@ futures = "0.3.31"
http-body-util = { version = "0.1.3", features = ["full"] } http-body-util = { version = "0.1.3", features = ["full"] }
hyper = { version = "1.6.0", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] }
hyper-util = { version = "0.1.11", features = ["full"] } hyper-util = { version = "0.1.11", features = ["full"] }
indexmap = "2.9.0"
rand = "0.8.5" rand = "0.8.5"
ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] } ratatui = { version = "0.29.0", features = ["unstable-backend-writer"] }
reqwest = { version = "0.12.15", features = ["rustls-tls"] } reqwest = { version = "0.12.15", features = ["rustls-tls"] }

View File

@ -1,7 +1,6 @@
use std::{io::Write, iter::once}; use std::{io::Write, iter::once};
use clap::Parser as _; use clap::Parser as _;
use indexmap::IndexMap;
use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend}; use ratatui::{Terminal, TerminalOptions, Viewport, layout::Rect, prelude::CrosstermBackend};
use russh::{ use russh::{
ChannelId, ChannelId,
@ -19,7 +18,7 @@ use crate::{
pub struct Handler { pub struct Handler {
all_tunnels: Tunnels, all_tunnels: Tunnels,
tunnels: IndexMap<String, Option<Tunnel>>, tunnels: Vec<Tunnel>,
user: Option<String>, user: Option<String>,
pty_channel: Option<ChannelId>, pty_channel: Option<ChannelId>,
@ -33,7 +32,7 @@ impl Handler {
pub fn new(all_tunnels: Tunnels) -> Self { pub fn new(all_tunnels: Tunnels) -> Self {
Self { Self {
all_tunnels, all_tunnels,
tunnels: IndexMap::new(), tunnels: Default::default(),
user: None, user: None,
pty_channel: None, pty_channel: None,
terminal: None, terminal: None,
@ -43,10 +42,8 @@ impl Handler {
} }
async fn set_access_all(&mut self, access: TunnelAccess) { async fn set_access_all(&mut self, access: TunnelAccess) {
for (_address, tunnel) in &self.tunnels { for tunnel in &self.tunnels {
if let Some(tunnel) = tunnel { tunnel.set_access(access.clone()).await;
tunnel.set_access(access.clone()).await;
}
} }
} }
@ -92,7 +89,7 @@ impl Handler {
async fn set_access_selection(&mut self, access: TunnelAccess) { async fn set_access_selection(&mut self, access: TunnelAccess) {
if let Some(selected) = self.selected { if let Some(selected) = self.selected {
if let Some((_, Some(tunnel))) = self.tunnels.get_index_mut(selected) { if let Some(tunnel) = self.tunnels.get_mut(selected) {
tunnel.set_access(access).await; tunnel.set_access(access).await;
} }
} else { } else {
@ -267,18 +264,16 @@ impl russh::server::Handler for Handler {
return Err(russh::Error::Inconsistent); return Err(russh::Error::Inconsistent);
}; };
let tunnel = Tunnel::new( let tunnel = self
session.handle(), .all_tunnels
address, .add_tunnel(session.handle(), address, *port, user)
*port, .await;
TunnelAccess::Private(user),
);
let (success, address) = self.all_tunnels.add_tunnel(address, tunnel.clone()).await;
let tunnel = if success { Some(tunnel) } else { None }; self.tunnels.push(tunnel);
self.tunnels.insert(address, tunnel);
Ok(success) // Technically forwarding has failed if tunnel.domain = None, however by lying to the ssh
// client we can retry in the future
Ok(true)
} }
async fn window_change_request( async fn window_change_request(

View File

@ -1,7 +1,6 @@
use std::cmp; use std::cmp;
use futures::StreamExt; use futures::StreamExt;
use indexmap::IndexMap;
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Constraint, Flex, Layout, Rect}, layout::{Constraint, Flex, Layout, Rect},
@ -25,12 +24,8 @@ fn command<'c>(key: &'c str, text: &'c str) -> Vec<Span<'c>> {
impl Renderer { impl Renderer {
// NOTE: This needs to be a separate function as the render functions can not be async // NOTE: This needs to be a separate function as the render functions can not be async
pub async fn update( pub async fn update(&mut self, tunnels: &[Tunnel], index: Option<usize>) {
&mut self, self.table_rows = futures::stream::iter(tunnels)
tunnels: &IndexMap<String, Option<Tunnel>>,
index: Option<usize>,
) {
self.table_rows = futures::stream::iter(tunnels.iter())
.then(tunnel::tui::to_row) .then(tunnel::tui::to_row)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await; .await;

View File

@ -8,8 +8,12 @@ use hyper::{
service::Service, service::Service,
}; };
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use indexmap::IndexMap; use std::{
use std::{collections::HashMap, ops::Deref, pin::Pin, sync::Arc}; collections::{HashMap, hash_map::Entry},
ops::Deref,
pin::Pin,
sync::Arc,
};
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use russh::{ use russh::{
@ -37,20 +41,12 @@ pub enum TunnelAccess {
pub struct Tunnel { pub struct Tunnel {
handle: Handle, handle: Handle,
name: String, name: String,
domain: Option<String>,
port: u32, port: u32,
access: Arc<RwLock<TunnelAccess>>, access: Arc<RwLock<TunnelAccess>>,
} }
impl Tunnel { impl Tunnel {
pub fn new(handle: Handle, name: impl Into<String>, port: u32, access: TunnelAccess) -> Self {
Self {
handle,
name: name.into(),
port,
access: Arc::new(RwLock::new(access)),
}
}
pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> { pub async fn open_tunnel(&self) -> Result<Channel<Msg>, russh::Error> {
trace!(tunnel = self.name, "Opening tunnel"); trace!(tunnel = self.name, "Opening tunnel");
self.handle self.handle
@ -65,6 +61,12 @@ impl Tunnel {
pub async fn is_public(&self) -> bool { pub async fn is_public(&self) -> bool {
matches!(*self.access.read().await, TunnelAccess::Public) matches!(*self.access.read().await, TunnelAccess::Public)
} }
pub fn get_address(&self) -> Option<String> {
self.domain
.clone()
.map(|domain| format!("{}.{domain}", self.name))
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -83,40 +85,57 @@ impl Tunnels {
} }
} }
pub async fn add_tunnel(&mut self, address: &str, tunnel: Tunnel) -> (bool, String) { pub async fn add_tunnel(
let mut all_tunnels = self.tunnels.write().await; &mut self,
handle: Handle,
name: impl Into<String>,
port: u32,
user: impl Into<String>,
) -> Tunnel {
let mut tunnel = Tunnel {
handle,
name: name.into(),
domain: Some(self.domain.clone()),
port,
access: Arc::new(RwLock::new(TunnelAccess::Private(user.into()))),
};
let address = if address == "localhost" { if tunnel.name == "localhost" {
// NOTE: It is technically possible to become stuck in this loop. // NOTE: It is technically possible to become stuck in this loop.
// However, that really only becomes a concern if a (very) high // However, that really only becomes a concern if a (very) high
// number of tunnels is open at the same time. // number of tunnels is open at the same time.
loop { loop {
let address = get_animal_name(); tunnel.name = get_animal_name().into();
let address = format!("{address}.{}", self.domain); if !self
if !all_tunnels.contains_key(&address) { .tunnels
break address; .read()
.await
.contains_key(&tunnel.get_address().expect("domain is set"))
{
break;
} }
trace!(tunnel = tunnel.name, "Already in use, picking new name");
} }
} else {
let address = format!("{address}.{}", self.domain);
if all_tunnels.contains_key(&address) {
return (false, address);
}
address
}; };
let address = tunnel.get_address().expect("domain is set");
trace!(tunnel = address, "Adding tunnel"); if let Entry::Vacant(e) = self.tunnels.write().await.entry(address) {
all_tunnels.insert(address.clone(), tunnel); trace!(tunnel = tunnel.name, "Adding tunnel");
e.insert(tunnel.clone());
} else {
trace!("Address already in use");
tunnel.domain = None
}
(true, address) tunnel
} }
pub async fn remove_tunnels(&mut self, tunnels: &IndexMap<String, Option<Tunnel>>) { pub async fn remove_tunnels(&mut self, tunnels: &[Tunnel]) {
let mut all_tunnels = self.tunnels.write().await; let mut all_tunnels = self.tunnels.write().await;
for (address, tunnel) in tunnels { for tunnel in tunnels {
if tunnel.is_some() { if let Some(address) = tunnel.get_address() {
trace!(address, "Removing tunnel"); trace!(tunnel.name, "Removing tunnel");
all_tunnels.remove(address); all_tunnels.remove(&address);
} }
} }
} }

View File

@ -6,22 +6,30 @@ use ratatui::text::Span;
use super::{Tunnel, TunnelAccess}; use super::{Tunnel, TunnelAccess};
pub fn header() -> Vec<Span<'static>> { pub fn header() -> Vec<Span<'static>> {
vec!["Access".into(), "Port".into(), "Address".into()] vec![
"Name".into(),
"Access".into(),
"Port".into(),
"Address".into(),
]
} }
pub async fn to_row((address, tunnel): (&String, &Option<Tunnel>)) -> Vec<Span<'static>> { pub async fn to_row(tunnel: &Tunnel) -> Vec<Span<'static>> {
let (access, port) = if let Some(tunnel) = tunnel { let access = match tunnel.access.read().await.deref() {
let access = match tunnel.access.read().await.deref() { TunnelAccess::Private(owner) => owner.clone().yellow(),
TunnelAccess::Private(owner) => owner.clone().yellow(), TunnelAccess::Protected => "PROTECTED".blue(),
TunnelAccess::Protected => "PROTECTED".blue(), TunnelAccess::Public => "PUBLIC".green(),
TunnelAccess::Public => "PUBLIC".green(),
};
(access, tunnel.port.to_string().into())
} else {
("FAILED".red(), "".into())
}; };
let address = format!("http://{address}").into();
vec![access, port, address] let address = tunnel
.get_address()
.map(|address| format!("http://{address}").into())
.unwrap_or("FAILED".red());
vec![
tunnel.name.clone().into(),
access,
tunnel.port.to_string().into(),
address,
]
} }