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.)
This commit is contained in:
John Doty 2023-04-30 06:51:37 -07:00
parent d3d7b4f137
commit ec130d38b9
2 changed files with 62 additions and 28 deletions

View file

@ -275,6 +275,7 @@ async fn client_main<Reader: AsyncRead + Unpin, Writer: AsyncWrite + Unpin>(
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<ui::UIEvent>) {
async fn client_connect_loop(
remote: &str,
sudo: bool,
events: mpsc::Sender<ui::UIEvent>,
) {
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<ui::UIEvent>) {
}
}
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) => ()
}
}

View file

@ -5,7 +5,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION");
fn usage() {
println!(indoc! {"
usage: fwd [--version] (<server> | browse <url>)
usage: fwd [options] (<server> | browse <url>)
To connect a client to a server that has an `fwd` installed in its path, run
`fwd <server>` on the client, where <server> 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
<url>` to open `<url>` 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<String>) -> 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]