Eugeny / russh

Rust SSH client & server library
https://docs.rs/russh
950 stars 106 forks source link

Unable to connect to the "example server" from Ubuntu 18.04 and 20.04 SSH. #127

Closed fadedbee closed 1 year ago

fadedbee commented 1 year ago

I'm using the example server from: https://docs.rs/russh/latest/russh/server/index.html but with a 60 second timeout. (Otherwise it exits after just one second.)

When I try: ssh localhost -p 2222 -v I see:

17:02 fadedbee@box ~ $ ssh localhost -p 2222 -v
OpenSSH_7.6p1 Ubuntu-4ubuntu0.7, OpenSSL 1.0.2n  7 Dec 2017
debug1: Reading configuration data /home/fadedbee/.ssh/config
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: Applying options for *
debug1: Connecting to localhost [127.0.0.1] port 2222.
debug1: Connection established.
debug1: identity file /home/fadedbee/.ssh/id_rsa type 0
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_rsa-cert type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_dsa type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_dsa-cert type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ecdsa type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ecdsa-cert type -1
debug1: identity file /home/fadedbee/.ssh/id_ed25519 type 3
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ed25519-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.7
debug1: Remote protocol version 2.0, remote software version russh_0.36.1
debug1: no match: russh_0.36.1
debug1: Authenticating to localhost:2222 as 'fadedbee'
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256@libssh.org
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ssh-ed25519 SHA256:M2gsNaJCJk+mYTqsas4NrjYHYsbd5OwnFN2MlQ1RRwY
debug1: checking without port identifier
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
ED25519 key fingerprint is SHA256:M2gsNaJCJk+mYTqsas4NrjYHYsbd5OwnFN2MlQ1RRwY.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
debug1: rekey after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: rekey after 134217728 blocks
debug1: Skipping ssh-dss key fadedbee@box - not in PubkeyAcceptedKeyTypes
Connection closed by 127.0.0.1 port 2222

17:02 fadedbee@box ~ $ 

What do I need to add to the example server to allow SSH clients to connect?

Eugeny commented 1 year ago

The example server will accept any ed25519 type client key, or also ssh-rsa if you build it with --all-features

fadedbee commented 1 year ago

Sorry, I tried cargo build --release --all-features followed by target/release/sshserver and the behaviour didn't change. Am I using --all-features correctly?

Eugeny commented 1 year ago

Yes but you still need to make sure you're using a supported key type to authenticate, or select it explicitly with ssh -i.

fadedbee commented 1 year ago

Thanks for your help with this. Does this mean that Russh is not suitable for an internet-facing SSH interface where you cannot control what types of keys the SSH clients have? (I was naively expecting to run the example server and to be able to connect from any SSH client less than 5 years old.)

Eugeny commented 1 year ago

Of course it is, ssh-rsa has been the default key type in OpenSSH for a decade now. Just generate one with ssh-keygen and you should be good to go.

fadedbee commented 1 year ago

Deleting my .ssh directory and making a new RSA key does not solve the problem. Am I missing something obvious here?

fadedbee@box:~$ ls -lA .ssh
ls: cannot access '.ssh': No such file or directory

fadedbee@box:~$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/fadedbee/.ssh/id_rsa): 
Created directory '/home/fadedbee/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/fadedbee/.ssh/id_rsa.
Your public key has been saved in /home/fadedbee/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:Fr2FptpCg3GjUfjTs+/uaZsbOCf3Tn5OpGth7AcTaYE fadedbee@box
The key's randomart image is:
+---[RSA 2048]----+
|     ..    .     |
|    ..   .E..    |
|    o.o.. + .o   |
|     *o.o+ o+    |
|    o o.So.o ..  |
|     . =..  *o   |
|      o =.+oo+.  |
|       . =oBooo  |
|         =O==+.  |
+----[SHA256]-----+

fadedbee@box:~$ ls -lA .ssh
total 8
-rw------- 1 fadedbee fadedbee 1679 Feb 18 12:10 id_rsa
-rw-r--r-- 1 fadedbee fadedbee  395 Feb 18 12:10 id_rsa.pub

fadedbee@box:~$ cat .ssh/id_rsa.pub 
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC33bxmYvGmIu0HDsdfWZTf3fE2SnveOd5h1VjBRkyBDVRbsiBiUBo7cGsSKkFCEBWBOa/jCivMJrm6EpsObi0iUJ8MQ1DRjGVIlYXfgSv/yNumdysur9Yuv0KYopVkEuxYyAr+ElCsuWUiQqxqEA+qIKEi3OFxSN8T1UiENR/R2NqaiN4IELYcbOcgjCJgsLGN0HpphijxR6swPlWR9PHRDd54mQIFawrtazS1OImEHpchSUghSaJpQ0CzRqWrLb2W5GbHQwhLDQr4GbYwZMT4ZTCWVLf9hCZVUEVxnzUBDXaDQrCZpdDfvGIwrELPQOT2/YTbMV6dfYNNuodzcZoP fadedbee@box

fadedbee@box:~$ ssh localhost -p 2222 -v
OpenSSH_7.6p1 Ubuntu-4ubuntu0.7, OpenSSL 1.0.2n  7 Dec 2017
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 19: Applying options for *
debug1: Connecting to localhost [127.0.0.1] port 2222.
debug1: Connection established.
debug1: identity file /home/fadedbee/.ssh/id_rsa type 0
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_rsa-cert type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_dsa type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_dsa-cert type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ecdsa type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ecdsa-cert type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ed25519 type -1
debug1: key_load_public: No such file or directory
debug1: identity file /home/fadedbee/.ssh/id_ed25519-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.7
debug1: Remote protocol version 2.0, remote software version russh_0.36.1
debug1: no match: russh_0.36.1
debug1: Authenticating to localhost:2222 as 'fadedbee'
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256@libssh.org
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ssh-ed25519 SHA256:LOsOQOLAdyJuJ51G22F/m7wv/Fn405EHzdr9EjXDIN0
debug1: checking without port identifier
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
ED25519 key fingerprint is SHA256:LOsOQOLAdyJuJ51G22F/m7wv/Fn405EHzdr9EjXDIN0.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:2222' (ED25519) to the list of known hosts.
debug1: rekey after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: rekey after 134217728 blocks
debug1: SSH2_MSG_EXT_INFO received
debug1: kex_input_ext_info: server-sig-algs=<ssh-rsa,ssh-ed25519,rsa-sha2-256,rsa-sha2-512>
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: password,publickey,hostbased,keyboard-interactive
debug1: Next authentication method: publickey
debug1: Offering public key: RSA SHA256:Fr2FptpCg3GjUfjTs+/uaZsbOCf3Tn5OpGth7AcTaYE /home/fadedbee/.ssh/id_rsa
debug1: Server accepts key: pkalg rsa-sha2-512 blen 279
debug1: Authentication succeeded (publickey).
Authenticated to localhost ([127.0.0.1]:2222).
debug1: channel 0: new [client-session]
debug1: Entering interactive session.
debug1: pledge: network
debug1: Sending environment.
debug1: Sending env LANG = en_GB.UTF-8
debug1: channel 0: free: client-session, nchannels 1
Connection to localhost closed by remote host.
Connection to localhost closed.
Transferred: sent 2988, received 1464 bytes, in 3.0 seconds
Bytes per second: sent 995.0, received 487.5
debug1: Exit status -1

fadedbee@box:~$ 
Eugeny commented 1 year ago

Looks like it did? Once you see "entering interactive session", you can press any keys and they should be broadcast to other connected clients.

fadedbee commented 1 year ago

No, there is no time, see: https://asciinema.org/a/xTQnJkoNcUeq54l3ZISyeIjaB

The full code of what I'm running, just in case I've used a wrong dependency or broken something is:

Cargo.toml:

[package]
name = "sshserver"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.69"
async-trait = "0.1.64"
env_logger = "0.10.0"
futures = "0.3.26"
log = "0.4.17"
russh = { version = "0.36.1", features = ["openssl", "vendored-openssl"] }
russh-keys = { version = "0.24.1", features = ["openssl", "vendored-openssl"] }
tokio = "1.25.0"

(I'd previously just used default features, but I then added all features, just in case.)

main.rs:

use async_trait::async_trait;
use futures::Future;
use log::debug;
use russh::server::{Auth, Msg, Session};
use russh::*;
use russh_keys::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client_key = russh_keys::key::KeyPair::generate_ed25519().unwrap();
    let client_pubkey = Arc::new(client_key.clone_public_key().unwrap());
    let mut config = russh::server::Config::default();
    config.connection_timeout = Some(std::time::Duration::from_secs(3));
    config.auth_rejection_time = std::time::Duration::from_secs(3);
    config
        .keys
        .push(russh_keys::key::KeyPair::generate_ed25519().unwrap());
    let config = Arc::new(config);
    let sh = Server {
        client_pubkey,
        clients: Arc::new(Mutex::new(HashMap::new())),
        id: 0,
    };
    tokio::time::timeout(
        std::time::Duration::from_secs(60),
        russh::server::run(config, ("0.0.0.0", 2222), sh),
    )
    .await
    .unwrap_or(Ok(()))?;

    Ok(())
}

#[derive(Clone)]
struct Server {
    client_pubkey: Arc<russh_keys::key::PublicKey>,
    clients: Arc<Mutex<HashMap<(usize, ChannelId), Channel<Msg>>>>,
    id: usize,
}

impl server::Server for Server {
    type Handler = Self;
    fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> Self {
        debug!("new client");
        let s = self.clone();
        self.id += 1;
        s
    }
}

#[async_trait]
impl server::Handler for Server {
    type Error = anyhow::Error;

    async fn channel_open_session(
        self,
        channel: Channel<Msg>,
        session: Session,
    ) -> Result<(Self, bool, Session), Self::Error> {
        {
            debug!("channel open session");
            let mut clients = self.clients.lock().unwrap();
            clients.insert((self.id, channel.id()), channel);
        }
        Ok((self, true, session))
    }
    async fn auth_publickey(
        self,
        _: &str,
        _: &key::PublicKey,
    ) -> Result<(Self, Auth), Self::Error> {
        Ok((self, server::Auth::Accept))
    }
    async fn data(
        self,
        channel: ChannelId,
        data: &[u8],
        mut session: Session,
    ) -> Result<(Self, Session), Self::Error> {
        {
            debug!("data: {data:?}");
            let mut clients = self.clients.lock().unwrap();
            for ((id, _channel_id), ref mut channel) in clients.iter_mut() {
                channel.data(data);
            }
        }
        Ok((self, session))
    }
}

(I've probably done a cargo fmt, as well as adding a couple of lines of logging/debugging and extending the timeout.)

The results of running the above are:

fadedbee@box ~/sshserver $ RUST_LOG=debug cargo run
warning: unused import: `futures::Future`
 --> src/main.rs:2:5
  |
2 | use futures::Future;
  |     ^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: unused variable: `channel`
  --> src/main.rs:78:9
   |
78 |         channel: ChannelId,
   |         ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_channel`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: unused variable: `id`
  --> src/main.rs:85:19
   |
85 |             for ((id, _channel_id), ref mut channel) in clients.iter_mut() {
   |                   ^^ help: if this is intentional, prefix it with an underscore: `_id`

warning: variable does not need to be mutable
  --> src/main.rs:80:9
   |
80 |         mut session: Session,
   |         ----^^^^^^^
   |         |
   |         help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` on by default

warning: field `client_pubkey` is never read
  --> src/main.rs:38:5
   |
37 | struct Server {
   |        ------ field in this struct
38 |     client_pubkey: Arc<russh_keys::key::PublicKey>,
   |     ^^^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default
   = note: `Server` has a derived impl for the trait `Clone`, but this is intentionally ignored during dead code analysis

warning: unused implementer of `futures::Future` that must be used
  --> src/main.rs:86:17
   |
86 |                 channel.data(data);
   |                 ^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: futures do nothing unless you `.await` or poll them

warning: `sshserver` (bin "sshserver") generated 6 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/sshserver`

fadedbee@box ~/sshserver $ 
fadedbee commented 1 year ago

I just noticed two 3 second timeouts which I'd overlooked:

    config.connection_timeout = Some(std::time::Duration::from_secs(3));
    config.auth_rejection_time = std::time::Duration::from_secs(3);

Changing these to 60 just makes disconnection take longer. No data is relayed between connections, or logged on the server.

fadedbee commented 1 year ago

Once the ssh client session exits, all of the keypresses get executed as commands, which makes me think that they have not been accepted by ssh and sent to the server. (Luckily I was typing asdfasfd not sudo rm -rf /.)

Eugeny commented 1 year ago

Figured it out! The example actually had the connection timeout set to 3 seconds - here is the fixed version:

use async_trait::async_trait;
use env_logger;
use futures::Future;
use log::debug;
use russh::server::{Auth, Msg, Session};
use russh::*;
use russh_keys::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    env_logger::init();
    let client_key = russh_keys::key::KeyPair::generate_ed25519().unwrap();
    let client_pubkey = Arc::new(client_key.clone_public_key().unwrap());
    let mut config = russh::server::Config::default();
    config.auth_rejection_time = std::time::Duration::from_secs(3);
    config
        .keys
        .push(russh_keys::key::KeyPair::generate_ed25519().unwrap());
    let config = Arc::new(config);
    let sh = Server {
        client_pubkey,
        clients: Arc::new(Mutex::new(HashMap::new())),
        id: 0,
    };
    tokio::time::timeout(
        std::time::Duration::from_secs(60),
        russh::server::run(config, ("0.0.0.0", 2222), sh),
    )
    .await
    .unwrap_or(Ok(()))?;

    Ok(())
}

#[derive(Clone)]
struct Server {
    client_pubkey: Arc<russh_keys::key::PublicKey>,
    clients: Arc<Mutex<HashMap<(usize, ChannelId), Channel<Msg>>>>,
    id: usize,
}

impl server::Server for Server {
    type Handler = Self;
    fn new_client(&mut self, _: Option<std::net::SocketAddr>) -> Self {
        debug!("new client");
        let s = self.clone();
        self.id += 1;
        s
    }
}

#[async_trait]
impl server::Handler for Server {
    type Error = anyhow::Error;

    async fn channel_open_session(
        self,
        channel: Channel<Msg>,
        session: Session,
    ) -> Result<(Self, bool, Session), Self::Error> {
        {
            debug!("channel open session");
            let mut clients = self.clients.lock().unwrap();
            clients.insert((self.id, channel.id()), channel);
        }
        Ok((self, true, session))
    }

    /// The client requests a shell.
    #[allow(unused_variables)]
    async fn shell_request(
        self,
        channel: ChannelId,
        mut session: Session,
    ) -> Result<(Self, Session), Self::Error> {
        session.request_success();
        Ok((self, session))
    }

    async fn auth_publickey(
        self,
        _: &str,
        _: &key::PublicKey,
    ) -> Result<(Self, Auth), Self::Error> {
        Ok((self, server::Auth::Accept))
    }
    async fn data(
        self,
        _channel: ChannelId,
        data: &[u8],
        mut session: Session,
    ) -> Result<(Self, Session), Self::Error> {
        debug!("data: {data:?}");
        {
            let mut clients = self.clients.lock().unwrap();
            for ((_, _channel_id), ref mut channel) in clients.iter_mut() {
                session.data(channel.id(), CryptoVec::from(data.to_vec()));
            }
        }
        Ok((self, session))
    }
}
fadedbee commented 1 year ago

Thanks, I'm now using your code above. The significant change was the addition of shell_request. (I'd spotted and changed the 3 second timeouts earlier.) It almost works now. SSH clients get their input echoed back to them, but not sent to each other. i.e. if I press a I see aa.

Eugeny commented 1 year ago

Before I jump into it any further, could you please try the included echoserver example? It's known working unlike this old example from the thrussh times.

fadedbee commented 1 year ago

I can confirm that the echoserver works correctly. Could you remove the "example server" from https://docs.rs/russh/0.36.1/russh/server/index.html ? Having a broken example as the first contact with this project will cause many prospective users to walk away.

I first attempted to use Thrush a few years ago, but couldn't get the example server to work back then, so thank your for all your help.