Skip to content

Commit 281fded

Browse files
committed
Add binding for channel_open_request_auth_agent_callback
This callback is required for implementing ssh agent forward as unlike X11 forward, there is no other way to establish a forwarding channel. The API design looks slightly convoluted, it's because 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. To do 3 we have to somehow steal a `struct ssh_channel*` from the user-owned channel. We decided to do so by create channel in the binding code, keep a ref and move it to user. Due to locking issues we have to take the Channel back if the user decided to not accept forward request. See SATEFY comment in bridge_channel_open_request_auth_agent_callback for details.
1 parent 5c135f7 commit 281fded

File tree

3 files changed

+102
-6
lines changed

3 files changed

+102
-6
lines changed

libssh-rs/src/channel.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ impl Drop for Channel {
4646
// Prevent any callbacks firing as part the remainder of this drop operation
4747
sys::ssh_remove_channel_callbacks(self.chan_inner, self._callbacks.as_mut());
4848
}
49+
if self.chan_inner.is_null() {
50+
return;
51+
}
4952
let (_sess, chan) = self.lock_session();
5053
unsafe {
5154
sys::ssh_channel_free(chan);

libssh-rs/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ pub enum Error {
1717
Sftp(crate::sftp::SftpError),
1818
}
1919

20+
#[derive(Error)]
21+
#[error("{0}")]
22+
pub struct RequestAuthAgentError(pub Error, pub crate::Channel);
23+
2024
/// Represents the result of a fallible operation
2125
pub type SshResult<T> = Result<T, Error>;
2226

@@ -53,3 +57,9 @@ impl From<std::ffi::NulError> for Error {
5357
Error::Fatal(err.to_string())
5458
}
5559
}
60+
61+
impl std::fmt::Debug for RequestAuthAgentError {
62+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63+
write!(f, "RequestAuthAgentError({})", self.0)
64+
}
65+
}

libssh-rs/src/lib.rs

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use std::os::unix::io::RawFd as RawSocket;
1717
#[cfg(windows)]
1818
use std::os::windows::io::RawSocket;
1919
use std::ptr::null_mut;
20-
use std::sync::Once;
2120
use std::sync::{Arc, Mutex, MutexGuard};
21+
use std::sync::{Once, Weak};
2222
use std::time::Duration;
2323

2424
mod channel;
@@ -72,9 +72,12 @@ fn initialize() -> SshResult<()> {
7272
}
7373

7474
pub(crate) struct SessionHolder {
75+
outer: Weak<Mutex<SessionHolder>>,
7576
sess: sys::ssh_session,
7677
callbacks: sys::ssh_callbacks_struct,
7778
auth_callback: Option<Box<dyn FnMut(&str, bool, bool, Option<String>) -> SshResult<String>>>,
79+
channel_open_request_auth_agent_callback:
80+
Option<Box<dyn FnMut(Channel) -> Result<(), RequestAuthAgentError>>>,
7881
}
7982
unsafe impl Send for SessionHolder {}
8083

@@ -197,11 +200,16 @@ impl Session {
197200
channel_open_request_x11_function: None,
198201
channel_open_request_auth_agent_function: None,
199202
};
200-
let sess = Arc::new(Mutex::new(SessionHolder {
201-
sess,
202-
callbacks,
203-
auth_callback: None,
204-
}));
203+
let sess = Arc::new_cyclic(|outer| {
204+
let outer = outer.clone();
205+
Mutex::new(SessionHolder {
206+
outer,
207+
sess,
208+
callbacks,
209+
auth_callback: None,
210+
channel_open_request_auth_agent_callback: None,
211+
})
212+
});
205213

206214
{
207215
let mut sess = sess.lock().unwrap();
@@ -274,6 +282,55 @@ impl Session {
274282
}
275283
}
276284

285+
unsafe extern "C" fn bridge_channel_open_request_auth_agent_callback(
286+
session: sys::ssh_session,
287+
userdata: *mut ::std::os::raw::c_void,
288+
) -> sys::ssh_channel {
289+
let result = std::panic::catch_unwind(|| -> SshResult<sys::ssh_channel> {
290+
let sess: &mut SessionHolder = &mut *(userdata as *mut SessionHolder);
291+
assert!(
292+
std::ptr::eq(session, sess.sess),
293+
"invalid callback invocation: session mismatch"
294+
);
295+
let cb = sess
296+
.channel_open_request_auth_agent_callback
297+
.as_mut()
298+
.unwrap();
299+
let chan = unsafe { sys::ssh_channel_new(session) };
300+
if chan.is_null() {
301+
return Err(sess
302+
.last_error()
303+
.unwrap_or_else(|| Error::fatal("ssh_channel_new failed")));
304+
}
305+
match cb(Channel::new(&sess.outer.upgrade().unwrap(), chan)) {
306+
// SAFETY: We steal a *mut sys::ssh_channel_struct here and let libssh
307+
// temporarily "borrows" it for an unspecified amount of time.
308+
// libssh is guaranteed to finish using it before returning from the outermost
309+
// libssh function call that triggered this callback. As such function call
310+
// always happens with Session locked and dropping Channel needs to lock the
311+
// session first, we can be sure that this *mut sys::ssh_channel_struct will not
312+
// be freed while libssh is still using it.
313+
Ok(_) => Ok(chan),
314+
Err(RequestAuthAgentError(err, mut chan_obj)) => {
315+
unsafe { sys::ssh_channel_free(chan_obj.chan_inner) };
316+
chan_obj.chan_inner = std::ptr::null_mut();
317+
Err(err)
318+
}
319+
}
320+
});
321+
match result {
322+
Err(err) => {
323+
eprintln!("Panic in request auth agent callback: {:?}", err);
324+
std::ptr::null_mut()
325+
}
326+
Ok(Err(err)) => {
327+
eprintln!("Error in request auth agent callback: {:#}", err);
328+
std::ptr::null_mut()
329+
}
330+
Ok(Ok(chan)) => chan,
331+
}
332+
}
333+
277334
/// Sets a callback that is used by libssh when it needs to prompt
278335
/// for the passphrase during public key authentication.
279336
/// This is NOT used for password or keyboard interactive authentication.
@@ -326,6 +383,32 @@ impl Session {
326383
sess.callbacks.auth_function = Some(Self::bridge_auth_callback);
327384
}
328385

386+
/// Sets a callback that is used by libssh when the remote side requests a new channel
387+
/// for SSH agent forwarding.
388+
/// The callback has the signature:
389+
///
390+
/// ```no_run
391+
/// use libssh_rs::RequestAuthAgentResult;
392+
/// fn callback(channel: Channel) -> RequestAuthAgentResult {
393+
/// unimplemented!()
394+
/// }
395+
/// ```
396+
///
397+
/// The callback should decide whether to allow the agent forward and if so, take ownership of
398+
/// the channel (and further move it elsewhere to handle agent protocol within). Otherwise or
399+
/// in case of an error, the callback should return the channel back as it is not possible to
400+
/// drop it in the callback.
401+
pub fn set_channel_open_request_auth_agent_callback<F>(&self, callback: F)
402+
where
403+
F: FnMut(Channel) -> Result<(), RequestAuthAgentError> + 'static,
404+
{
405+
let mut sess = self.lock_session();
406+
sess.channel_open_request_auth_agent_callback
407+
.replace(Box::new(callback));
408+
sess.callbacks.channel_open_request_auth_agent_function =
409+
Some(Self::bridge_channel_open_request_auth_agent_callback);
410+
}
411+
329412
/// Create a new channel.
330413
/// Channels are used to handle I/O for commands and forwarded streams.
331414
pub fn new_channel(&self) -> SshResult<Channel> {

0 commit comments

Comments
 (0)