#client-server #ssh #client-connection #thrussh #handler #session #channel

已删除 thrussh_pub_conntect_stream

客户端和服务器SSH库

0.26.2 2020年6月2日

#8 in #thrussh

Apache-2.0

240KB
5.5K SLoC

基于 tokio/futures 的服务器和客户端 SSH 异步库。

使用此库的常规方式是创建 处理器,即实现 client::Handler 的类型(客户端)和 server::Handler 的类型(服务器)。

编写服务器

在服务器的特定情况下,服务器必须实现 server::Server,这是一个用于创建新的 server::Handler 的 trait。在 server 模块中,需要关注的主要类型是 Session(当然还有 Config)。

以下是一个示例服务器,该服务器将每个客户端的输入转发给所有其他客户端

extern crate thrussh;
extern crate thrussh_keys;
extern crate futures;
extern crate tokio;
use std::sync::{Mutex, Arc};
use thrussh::*;
use thrussh::server::{Auth, Session};
use thrussh_keys::*;
use std::collections::HashMap;
use futures::Future;

#[tokio::main]
async fn main() {
    let client_key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap();
    let client_pubkey = Arc::new(client_key.clone_public_key());
    let mut config = thrussh::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(thrussh_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(1),
       thrussh::server::run(config, "0.0.0.0:2222", sh)
    ).await.unwrap_or(Ok(()));
}

#[derive(Clone)]
struct Server {
    client_pubkey: Arc<thrussh_keys::key::PublicKey>,
    clients: Arc<Mutex<HashMap<(usize, ChannelId), thrussh::server::Handle>>>,
    id: usize,
}

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

impl server::Handler for Server {
    type FutureAuth = futures::future::Ready<Result<server::Auth, anyhow::Error>>;
    type FutureUnit = futures::future::Ready<Result<(), anyhow::Error>>;
    type FutureBool = futures::future::Ready<Result<bool, anyhow::Error>>;

    fn finished_auth(&mut self, auth: Auth) -> Self::FutureAuth {
        futures::future::ready(Ok(auth))
    }
    fn finished_bool(&mut self, b: bool, s: &mut Session) -> Self::FutureBool {
        futures::future::ready(Ok(b))
    }
    fn finished(&mut self, s: &mut Session) -> Self::FutureUnit {
        futures::future::ready(Ok(()))
    }
    fn channel_open_session(&mut self, channel: ChannelId, session: &mut Session) -> Self::FutureUnit {
        {
            let mut clients = self.clients.lock().unwrap();
            clients.insert((self.id, channel), session.handle());
        }
        self.finished(session)
    }
    fn auth_publickey(&mut self, _: &str, _: &key::PublicKey) -> Self::FutureAuth {
        self.finished_auth(server::Auth::Accept)
    }
    fn data(&mut self, channel: ChannelId, data: &[u8], mut session: &mut Session) -> Self::FutureUnit {
        {
            let mut clients = self.clients.lock().unwrap();
            for ((id, channel), ref mut s) in clients.iter_mut() {
                if *id != self.id {
                    s.data(*channel, CryptoVec::from_slice(data));
                }
            }
        }
        session.data(channel, data);
        self.finished(session)
    }
}

注意对 session.handle() 的调用,这允许在事件循环外部保持对客户端的引用。此功能内部使用 futures::sync::mpsc 通道实现。

请注意,这只是一个玩具服务器。特别是

  • s.data 返回错误时,它不处理错误,即当客户端消失时

  • 每次新的连接都会增加 id 字段。尽管为了使它饱和可能需要每秒很多连接并且持续很长时间,但可能存在更好的处理方式以避免冲突。

实现客户端

可能令人惊讶的是,Thrussh 用于实现客户端的数据类型相对于服务器来说相对更复杂。这主要与客户端通常以同步方式(在 SSH 的情况下,我们可以考虑发送 shell 命令)和异步方式(因为服务器有时可能会发送未请求的消息)使用有关,因此需要处理多个接口。

client模块中,重要的类型是SessionConnection。通常使用Connection向服务器发送命令并等待响应,其中包含一个Session。当客户端收到数据时,将Session传递给Handler

extern crate thrussh;
extern crate thrussh_keys;
extern crate futures;
extern crate tokio;
extern crate env_logger;
use std::sync::Arc;
use thrussh::*;
use thrussh::server::{Auth, Session};
use thrussh_keys::*;
use futures::Future;
use std::io::Read;


struct Client {
}

impl client::Handler for Client {
    type FutureUnit = futures::future::Ready<Result<(), anyhow::Error>>;
    type FutureBool = futures::future::Ready<Result<bool, anyhow::Error>>;

    fn finished_bool(&mut self, b: bool) -> Self::FutureBool {
        futures::future::ready(Ok(b))
    }
    fn finished(&mut self) -> Self::FutureUnit {
        futures::future::ready(Ok(()))
    }
   fn check_server_key(&mut self, server_public_key: &key::PublicKey) -> Self::FutureBool {
       println!("check_server_key: {:?}", server_public_key);
       self.finished_bool(true)
   }
   fn channel_open_confirmation(&mut self, channel: ChannelId, session: &mut client::Session) -> Self::FutureUnit {
       println!("channel_open_confirmation: {:?}", channel);
       self.finished()
   }
   fn data(&mut self, channel: ChannelId, data: &[u8], session: &mut client::Session) -> Self::FutureUnit {
       println!("data on channel {:?}: {:?}", channel, std::str::from_utf8(data));
       self.finished()
   }
}

#[tokio::main]
async fn main() {
let config = thrussh::client::Config::default();
let config = Arc::new(config);
let sh = Client{};

let key = thrussh_keys::key::KeyPair::generate_ed25519().unwrap();
let mut agent = thrussh_keys::agent::client::AgentClient::connect_env().await.unwrap();
agent.add_identity(&key, &[]).await.unwrap();
let mut session = thrussh::client::connect(config, "127.0.0.1:2222", sh).await.unwrap();
if session.authenticate_future("pe", key.clone_public_key(), agent).await.unwrap() {
    let mut channel = session.channel_open_session().await.unwrap();
    channel.data("Hello, world!").await.unwrap();
    if let Some(msg) = channel.wait().await {
        println!("{:?}", msg)
    }
}
}

使用非套接字IO/编写隧道

实现SSH隧道的一种简单方法,如OpenSSH的ProxyCommand,是使用thrussh-config库,并使用该库中的Stream::tcp_connectStream::proxy_command方法。该库是Thrussh之上的一个非常轻量级的层,只为外部命令实现用于套接字的特性。

SSH协议

如果我们排除由Thrussh在幕后处理的密钥交换和认证阶段,SSH协议的其余部分相对简单:客户端和服务器打开通道,这些通道只是用于在单个连接中并行处理多个请求的整数。一旦客户端通过调用client::Connection中的许多channel_open_…方法之一获得了ChannelId,客户端就可以向服务器发送exec请求和数据。

一个简单的客户端只需请求服务器运行一个命令,通常会先调用client::Connection::channel_open_session,然后是client::Connection::exec,然后可能是多次调用client::Connection::data向命令的标准输入发送数据,最后调用Connection::channel_eofConnection::channel_close

设计原则

这个库的主要目标是简洁,以及库代码的大小和可读性。此外,这个库分为Thrussh和Thrussh-keys,Thrussh实现SSH客户端和服务器的主要逻辑,而Thrussh-keys实现对加密原语的调用。

一个非目标是实现自SSH初始发布以来发布的所有可能的加密算法。技术债务很容易获得,我们需要一个非常强大的理由来反对这个原则。如果您正在从头开始设计一个系统,我们强烈建议您考虑最近发布的加密原语,如用于公钥加密的Ed25519,以及用于对称加密和MAC的Chacha20-Poly1305。

事件循环的内部细节

服务器或客户端会话的读写方法通常既不返回Result也不返回Future,这看起来可能有些奇怪。这是因为发送到远程端的数据被缓冲,因为需要先加密,而加密是在缓冲区上工作的,并且对于许多算法,不是原地工作。

因此,事件循环会等待传入的数据包,通过调用提供的Handler来对这些数据包做出反应,该Handler填充一些缓冲区。如果缓冲区非空,事件循环将它们发送到套接字,刷新套接字,清空缓冲区,然后重新开始。在服务器特殊情况下,当没有可读的传入数据包时,通过server::Handle发送的非请求消息会被处理。

依赖关系

~25MB
~242K SLoC