From ec130d38b913bb254b64a6bead8b87889735f47f Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 30 Apr 2023 06:51:37 -0700 Subject: [PATCH] Support running server side with sudo This allows forwarding ports that you would otherwise not be able to see. More dangerous, probably not what you want most of the time, but OK for now. (I continue to resist adding clap as a dependency.) --- src/client/mod.rs | 21 ++++++++++----- src/main.rs | 69 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index da3a15d..567a186 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -275,6 +275,7 @@ async fn client_main( async fn spawn_ssh( server: &str, + sudo: bool, ) -> Result<(tokio::process::Child, u16), std::io::Error> { let socks_port = { let listener = TcpListener::bind("127.0.0.1:0").await?; @@ -285,9 +286,11 @@ async fn spawn_ssh( cmd.arg("-T") .arg("-D") .arg(socks_port.to_string()) - .arg(server) - .arg("fwd") - .arg("--server"); + .arg(server); + if sudo { + cmd.arg("sudo"); + } + cmd.arg("fwd").arg("--server"); cmd.stdout(std::process::Stdio::piped()); cmd.stdin(std::process::Stdio::piped()); @@ -314,12 +317,16 @@ fn is_sigint(status: std::process::ExitStatus) -> bool { } } -async fn client_connect_loop(remote: &str, events: mpsc::Sender) { +async fn client_connect_loop( + remote: &str, + sudo: bool, + events: mpsc::Sender, +) { loop { _ = events.send(ui::UIEvent::Disconnected).await; let (mut child, socks_port) = - spawn_ssh(remote).await.expect("failed to spawn"); + spawn_ssh(remote, sudo).await.expect("failed to spawn"); let mut stderr = child .stderr @@ -377,7 +384,7 @@ async fn client_connect_loop(remote: &str, events: mpsc::Sender) { } } -pub async fn run_client(remote: &str) { +pub async fn run_client(remote: &str, sudo: bool) { let (event_sender, event_receiver) = mpsc::channel(1024); _ = log::set_boxed_logger(ui::Logger::new(event_sender.clone())); log::set_max_level(LevelFilter::Info); @@ -395,7 +402,7 @@ pub async fn run_client(remote: &str) { // Start the reconnect loop. tokio::select! { _ = ui.run() => (), - _ = client_connect_loop(remote, event_sender) => () + _ = client_connect_loop(remote, sudo, event_sender) => () } } diff --git a/src/main.rs b/src/main.rs index 243f4f4..667ff1c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); fn usage() { println!(indoc! {" -usage: fwd [--version] ( | browse ) +usage: fwd [options] ( | browse ) To connect a client to a server that has an `fwd` installed in its path, run `fwd ` on the client, where is the name of the server to @@ -13,6 +13,15 @@ connect to. On a server that already has a client connected to it you can use `fwd browse ` to open `` in the default browser of the client. + +Options: + --version Print the version of fwd and exit + --sudo, -s Run the server side of fwd with `sudo`. This allows the + client to forward ports that are open by processes being + run under other accounts (e.g., docker containers being + run as root), but requires sudo access on the server and + *might* end up forwarding ports that you do not want + forwarded (e.g., port 22 for sshd, or port 53 for systemd.) "}); } @@ -21,33 +30,46 @@ enum Args { Help, Version, Server, - Client(String), + Client(String, bool), Browse(String), Error, } fn parse_args(args: Vec) -> Args { - // Look for help; allow it to come anywhere because sometimes you just - // want to jam it on the end of an existing command line. - for arg in &args { + let mut server = None; + let mut sudo = None; + let mut rest = Vec::new(); + + for arg in args.into_iter().skip(1) { if arg == "--help" || arg == "-?" || arg == "-h" { return Args::Help; + } else if arg == "--version" { + return Args::Version; + } else if arg == "--server" { + server = Some(true) + } else if arg == "--sudo" || arg == "-s" { + sudo = Some(true) + } else { + rest.push(arg) } } - // No help, parse for reals. - if args.len() >= 2 && args[1] == "--version" { - Args::Version - } else if args.len() == 2 && &args[1] == "--server" { - Args::Server - } else if args.len() == 3 && args[1] == "browse" { - Args::Browse(args[2].to_string()) - } else { - if args.len() != 2 { - Args::Error + if server.unwrap_or(false) { + if rest.len() == 0 && sudo.is_none() { + Args::Server } else { - Args::Client(args[1].to_string()) + Args::Error } + } else if rest.len() > 1 && rest[0] == "browse" { + if rest.len() == 2 { + Args::Browse(rest[1].to_string()) + } else { + Args::Error + } + } else if rest.len() == 1 { + Args::Client(rest[0].to_string(), sudo.unwrap_or(false)) + } else { + Args::Error } } @@ -66,8 +88,8 @@ async fn main() { Args::Browse(url) => { fwd::browse_url(&url).await; } - Args::Client(server) => { - fwd::run_client(&server).await; + Args::Client(server, sudo) => { + fwd::run_client(&server, sudo).await; } Args::Error => { usage(); @@ -111,13 +133,18 @@ mod tests { assert_arg_parse!(&["browse", "google.com", "what"], Args::Error); assert_arg_parse!(&["a", "b"], Args::Error); assert_arg_parse!(&["--server", "something"], Args::Error); + assert_arg_parse!(&["--server", "--sudo"], Args::Error); + assert_arg_parse!(&["--server", "-s"], Args::Error); } #[test] fn client() { - assert_arg_parse!(&["foo.com"], Args::Client(_)); - assert_arg_parse!(&["a"], Args::Client(_)); - assert_arg_parse!(&["browse"], Args::Client(_)); + assert_arg_parse!(&["foo.com"], Args::Client(_, false)); + assert_arg_parse!(&["a"], Args::Client(_, false)); + assert_arg_parse!(&["browse"], Args::Client(_, false)); + assert_arg_parse!(&["foo.com", "--sudo"], Args::Client(_, true)); + assert_arg_parse!(&["a", "-s"], Args::Client(_, true)); + assert_arg_parse!(&["-s", "browse"], Args::Client(_, true)); } #[test]