Skip to content

Commit

Permalink
Add binding for channel_open_request_auth_agent_callback
Browse files Browse the repository at this point in the history
This callback is required for implementing ssh agent forward as unlike
X11 forward, there is no other way to establish a forwarding channel.

In libssh:

1. Callback is triggered while handling protocol packets in other libssh
   call.
2. The callback creates a new channel and prepare for bidirectional
   forwarding between it and ssh agent.
3. The callback then returns a borrow of the newly created channel for
   libssh to make reply to the remote side.

However, the callback-based flow does not really fit our Rust binding
design: during callback we have SessionHolder locked, so it's really
hard to do anything without introducing lock re-entrancy issues, plus
that it demands us to return a temporary borrow of something owned by
Rust side whose lifetime is tricky to model.

Instead, we try to turn the callback-based style back to something
resembling `ssh_channel_accept_x11` by buffering pending channels and
let users fetch them later in a normal context.
  • Loading branch information
Riatre committed May 7, 2024
1 parent 5c135f7 commit a6df87b
Showing 1 changed file with 87 additions and 0 deletions.
87 changes: 87 additions & 0 deletions libssh-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub(crate) struct SessionHolder {
sess: sys::ssh_session,
callbacks: sys::ssh_callbacks_struct,
auth_callback: Option<Box<dyn FnMut(&str, bool, bool, Option<String>) -> SshResult<String>>>,
channel_open_request_auth_agent_callback: Option<Box<dyn FnMut() -> bool>>,
pending_agent_forward_channels: Vec<sys::ssh_channel>,
}
unsafe impl Send for SessionHolder {}

Expand All @@ -87,6 +89,12 @@ impl std::ops::Deref for SessionHolder {

impl Drop for SessionHolder {
fn drop(&mut self) {
for chan in self.pending_agent_forward_channels.drain(..) {
unsafe {
// We have no callbacks on these channels, no need to cleanup.
sys::ssh_channel_free(chan);
}
}
unsafe {
sys::ssh_free(self.sess);
}
Expand Down Expand Up @@ -201,6 +209,8 @@ impl Session {
sess,
callbacks,
auth_callback: None,
channel_open_request_auth_agent_callback: None,
pending_agent_forward_channels: Vec::new(),
}));

{
Expand Down Expand Up @@ -274,6 +284,48 @@ impl Session {
}
}

unsafe extern "C" fn bridge_channel_open_request_auth_agent_callback(
session: sys::ssh_session,
userdata: *mut ::std::os::raw::c_void,
) -> sys::ssh_channel {
let result = std::panic::catch_unwind(|| -> SshResult<sys::ssh_channel> {
let sess: &mut SessionHolder = &mut *(userdata as *mut SessionHolder);
assert!(
std::ptr::eq(session, sess.sess),
"invalid callback invocation: session mismatch"
);
let cb = sess
.channel_open_request_auth_agent_callback
.as_mut()
.unwrap();
if !cb() {
return Err(Error::RequestDenied(
"callback denied agent forward".to_owned(),
));
}
let chan = unsafe { sys::ssh_channel_new(session) };
if chan.is_null() {
return Err(sess
.last_error()
.unwrap_or_else(|| Error::fatal("ssh_channel_new failed")));
}
// We are guarenteed to be holding a session lock here.
sess.pending_agent_forward_channels.push(chan);
Ok(chan)
});
match result {
Err(err) => {
eprintln!("Panic in request auth agent callback: {:?}", err);
std::ptr::null_mut()
}
Ok(Err(err)) => {
eprintln!("Error in request auth agent callback: {:#}", err);
std::ptr::null_mut()
}
Ok(Ok(chan)) => chan,
}
}

/// Sets a callback that is used by libssh when it needs to prompt
/// for the passphrase during public key authentication.
/// This is NOT used for password or keyboard interactive authentication.
Expand Down Expand Up @@ -326,6 +378,41 @@ impl Session {
sess.callbacks.auth_function = Some(Self::bridge_auth_callback);
}

/// Sets a callback that is used by libssh when the remote side requests a new channel
/// for SSH agent forwarding.
/// The callback has the signature:
///
/// ```no_run
/// fn callback() -> bool {
/// unimplemented!()
/// }
/// ```
///
/// The callback should decide whether agent forwarding is allowed and possible now.
/// If the callback returns `true`, a new channel will be created and bound to remote side.
/// Since it is not possible to do anything with the channel in the callback, it is not passed
/// to the callback directly, and you are supposed to retrieve it later by using the
/// `pop_auth_agent_channel` function and connect it to an agent.
pub fn set_channel_open_request_auth_agent_callback<F>(&self, callback: F)
where
F: FnMut() -> bool + 'static,
{
let mut sess = self.lock_session();
sess.channel_open_request_auth_agent_callback
.replace(Box::new(callback));
sess.callbacks.channel_open_request_auth_agent_function =
Some(Self::bridge_channel_open_request_auth_agent_callback);
}

// Accept an auth agent forward channel.
// Returns a `Channel` bound to the remote side SSH agent client, or `None` if no pending
// request from the server.
pub fn accept_agent_forward(&self) -> Option<Channel> {
let mut sess = self.lock_session();
let chan = sess.pending_agent_forward_channels.pop()?;
Some(Channel::new(&self.sess, chan))
}

/// Create a new channel.
/// Channels are used to handle I/O for commands and forwarded streams.
pub fn new_channel(&self) -> SshResult<Channel> {
Expand Down

0 comments on commit a6df87b

Please sign in to comment.