Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to serve basic shell request using russh #162

Closed
michaelfortunato opened this issue Jul 5, 2023 · 4 comments
Closed

How to serve basic shell request using russh #162

michaelfortunato opened this issue Jul 5, 2023 · 4 comments

Comments

@michaelfortunato
Copy link

Hi all,
I am trying to write a very basic ssh daemon. Essentially what it does is

  1. Listens for clients
  2. For each client, forks a process (where the program is inter-active (std::process:Command("/bin/bash") for instance))
  3. Connects the stdin and stdout of the forked process to the client.

Here is my approach so far. I will

  1. Override server::Handler::channel_open_session
  2. Allocate a pty in that function
  3. Launch the child process
  4. Set the child process's stdin and stdout to be the pty slave file.
  5. On a tokio thread copy the master file to the channel
  6. On another tokio thread, copy the channel to the master file
use std::collections::HashMap;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::{os, process, thread};

use async_trait::async_trait;
use pty::fork::Fork;
use russh::server::Auth;
use russh::server::{Msg, Session};
use russh::*;
use russh_keys::*;

impl server::Handler for Server<'_> {

    async fn channel_open_session(
        self,
        channel: Channel<Msg>,
        session: Session,
    ) -> Result<(Self, bool, Session), Self::Error> {
        let fork = Fork::from_ptmx().unwrap();
        if let Some(mut master) = fork.is_parent().ok() {
            tokio::spawn(async {
                std::io::copy(&mut channel, &mut master); // This does not work because channel does not implement Read
            });
            tokio::spawn(async {
                std::io::copy(&mut master, channel); // This does not work because channel does not implement Write
            });

        } else {
            let Some(mut child) = fork.is_child().ok()
            Command::new("/bin/bash").stdin(child).stdout(child).spawn();
        }
        Ok((self, true, session))
    }

The above code won't compile because channel does not implement the Read and Write functions for io::copy.

My questions are

  1. Is my approach correct in general for the problem I am trying to solve (ie. allocate an interactive process for each client)
  2. If my approach makes sense, is there a struct in russh that implements Read and Write?
@michaelfortunato michaelfortunato changed the title How to implement basic shell request using russh How to serve basic shell request using russh Jul 5, 2023
@Eugeny
Copy link
Owner

Eugeny commented Jul 6, 2023

Your approach in general is ok. Channel doesn't implement Read/Write, but you can turn it into a tokio::AsyncRead + AsyncWrite via Channel::into_stream

You can't use std::io::copy with it though, and will have to write a copy loop yourself using tokio::select!, since the ChannelStream can't be split into Read and Write halves.

@michaelfortunato
Copy link
Author

michaelfortunato commented Jul 6, 2023

I see. I was thinking I could do something like

tokio::io::copy(channel, master)
tokio::io::copy(master, channel)

Because tokio::io::copy does io copy with AsyncRead and AsyncWrite streams.

But master does not support AsyncRead nor AsyncWrite, and as you noted channel cannot be split so the borrow checker would complain.

@michaelfortunato
Copy link
Author

In fact it seems like tokio has a tutorial somewhat related to your suggestion on manual copy.

https://tokio.rs/tokio/tutorial/io

@lowlevl
Copy link
Contributor

lowlevl commented Sep 12, 2023

Hi there,

I'd like propose a feature related to this issue, for context I'm trying to create a git SSH remote with russh and I've tried the Channel::into_stream method.
Unfortunately it has a few issues, namely

  • it exits on receiving EOF from the client without finishing to flush the AsyncWrite side, this is an issue in my case because git closes the stream as soon as it has finished to transmit while it still awaits to receive data.
  • it sends back a 0-length packet directly to the AsyncRead side which, in my case seems to confuse the stdin of my program very much.

I'd like to propose to work on a

impl Channel {
    pub fn into_io_parts(self) -> (ChannelTx, ChannelRx) {
        /* ... */
    }
}

impl AsyncWrite for ChannelTx {/* ... */}
impl AsyncRead for ChannelRx {/* ... */}

method to solve both of these problems.

Then a basic I/O bidirectional stream could be established with

let child = Command::new("cat").spawn();
let (mut tx, mut rx) = channel.into_io_parts();

tokio::try_join!(
    tokio::io::copy(&mut child.stdout.expect("Missing stdout"), &mut tx),
    tokio::io::copy(&mut rx, &mut child.stdin.expect("Missing stdin")),
).await.expect("Copy failed !");

Would you be open to this contribution ?
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants