diff --git a/tool/fdpass-teleport/.gitattributes b/tool/fdpass-teleport/.gitattributes new file mode 100644 index 0000000000000..b1d9e273eb1ba --- /dev/null +++ b/tool/fdpass-teleport/.gitattributes @@ -0,0 +1 @@ +/Cargo.lock -merge diff --git a/tool/fdpass-teleport/.gitignore b/tool/fdpass-teleport/.gitignore new file mode 100644 index 0000000000000..ea8c4bf7f35f6 --- /dev/null +++ b/tool/fdpass-teleport/.gitignore @@ -0,0 +1 @@ +/target diff --git a/tool/fdpass-teleport/Cargo.lock b/tool/fdpass-teleport/Cargo.lock new file mode 100644 index 0000000000000..8fa06fae09e11 --- /dev/null +++ b/tool/fdpass-teleport/Cargo.lock @@ -0,0 +1,95 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fdpass-teleport" +version = "0.1.0" +dependencies = [ + "nix", + "simple-eyre", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "simple-eyre" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a" +dependencies = [ + "eyre", + "indenter", +] diff --git a/tool/fdpass-teleport/Cargo.toml b/tool/fdpass-teleport/Cargo.toml new file mode 100644 index 0000000000000..7ccfb2f201fcb --- /dev/null +++ b/tool/fdpass-teleport/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fdpass-teleport" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +nix = { version = "0.29.0", features = ["socket", "uio", "fs"] } +simple-eyre = "0.3.1" + +[profile.release] +codegen-units = 1 +lto = "fat" +panic = "abort" +strip = "symbols" diff --git a/tool/fdpass-teleport/src/main.rs b/tool/fdpass-teleport/src/main.rs new file mode 100644 index 0000000000000..13331f4411bf0 --- /dev/null +++ b/tool/fdpass-teleport/src/main.rs @@ -0,0 +1,109 @@ +// Teleport +// Copyright (C) 2024 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use nix::{ + errno::Errno, + fcntl::{self, OFlag}, + libc, + sys::socket::{self, ControlMessage, MsgFlags}, +}; +use simple_eyre::eyre::{self, OptionExt, Result, WrapErr}; +use std::{ + env, + io::{IoSlice, Write}, + os::{ + fd::AsRawFd, + unix::{ffi::OsStrExt, net::UnixStream}, + }, + process, +}; + +fn main() -> Result<()> { + simple_eyre::install()?; + + let mut args = env::args_os(); + + if args.len() != 3 { + let pkg_name = env!("CARGO_PKG_NAME"); + let version = option_env!("VERSION").unwrap_or("unknown"); + + let argv0 = args.next(); + let argv0 = match &argv0 { + Some(o) => o.to_string_lossy(), + None => pkg_name.into(), + }; + + eprint!( + "{pkg_name} (version {version})\n\nusage: {argv0} \n", + ); + process::exit(libc::EXIT_FAILURE); + } + + let mux_path = args.nth(1).ok_or_eyre("missing mux path")?; + let mut target = args.next().ok_or_eyre("missing connection target")?; + target.push("\0"); + + // in OpenSSH ProxyCommand+ProxyUseFdPass the program is executed with a + // unix domain socket as stdout, which we can check with a getsockname(); + // we'll get a ENOTSOCK if stdout is not a socket, or EINVAL if the address + // returned by getsockname() is not AF_UNIX (i.e. stdout is a socket but not + // a unix domain socket) + socket::getsockname::(libc::STDOUT_FILENO) + .wrap_err("stdout is not a unix socket")?; + + // to not have to bother with poll() for the later sendmsg() we have to + // confirm that the socket is set to blocking mode, or we might end up + // busylooping with EAGAIN; OpenSSH at the time of writing (9.7) will give + // the ProxyCommand a blocking socketpair() half, so we should be good + let fl = fcntl::fcntl(libc::STDOUT_FILENO, fcntl::F_GETFL) + .wrap_err("could not check stdout for blocking mode")?; + let fl = OFlag::from_bits_retain(fl); + if fl.contains(OFlag::O_NONBLOCK) { + let mut fl = fl; + fl.set(OFlag::O_NONBLOCK, false); + fcntl::fcntl(libc::STDOUT_FILENO, fcntl::F_SETFL(fl)) + .wrap_err("could not set stdout to blocking mode")?; + } + + let mut mux_conn = UnixStream::connect(mux_path).wrap_err("could not connect to mux")?; + // we added a final NUL to target above + mux_conn + .write_all(target.as_bytes()) + .wrap_err("could not send connection target to mux")?; + + // we can now pass the connection to OpenSSH (or whoever launched us) over + // stdout, sending a byte of actual data together with the connection's file + // descriptor; logic lifted from OpenBSD's netcat fdpass code + // (https://github.com/openbsd/src/blob/master/usr.bin/nc/netcat.c) + loop { + match socket::sendmsg::<()>( + libc::STDOUT_FILENO, + &[IoSlice::new(&[0])], + &[ControlMessage::ScmRights(&[mux_conn.as_raw_fd()])], + MsgFlags::empty(), + None, + ) { + Ok(1) => break, + Ok(s) => eyre::bail!("unexpected sendmsg return value {s}"), + Err(Errno::EAGAIN | Errno::EINTR) => continue, + Err(e) => Err(e).wrap_err("could not pass connection to stdout")?, + }; + } + + // returning would close and deallocate things, and there's just no need for + // that + process::exit(libc::EXIT_SUCCESS); +}