0.22.0 |
|
---|
#9 in #thrussh
235KB
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, failure::Error>>;
type FutureUnit = futures::future::Ready<Result<(), failure::Error>>;
type FutureBool = futures::future::Ready<Result<bool, failure::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
模块中,重要的类型有 Session
和 Connection
。通常使用 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<(), failure::Error>>;
type FutureBool = futures::future::Ready<Result<bool, failure::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 隧道的一种简单方法是使用 thrussh-config
包,并使用该包中的 Stream::tcp_connect
或 Stream::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_eof
和 Connection::channel_close
。
设计原则
这个库的主要目标是简洁性,以及库代码的尺寸和可读性。此外,该库分为 Thrussh 和 Thrussh-keys,Thrussh 实现了 SSH 客户端和服务器的主要逻辑,而 Thrussh-keys 实现了对加密原语的调用。
一个非目标是实现自 SSH 初始发布以来发布的所有可能的加密算法。技术债务很容易积累,并且我们需要一个非常充分的理由来违反这一原则。如果您正在从头开始设计一个系统,我们强烈建议您考虑像 Ed25519 这样的最新公钥加密原语,以及用于对称加密和 MAC 的 Chacha20-Poly1305。
事件循环的内部细节
服务器或客户端会话的读写方法通常既不返回 Result
也不返回 Future
,这看起来可能有点奇怪。这是因为发送到远程端的数据被缓冲,因为它需要先被加密,而加密是在缓冲区上进行的,对于许多算法来说,不是在原地进行的。
因此,事件循环会不断等待传入的数据包,通过调用提供的 Handler
来对其做出反应,该处理器填充一些缓冲区。如果缓冲区非空,则事件循环将它们发送到套接字,刷新套接字,清空缓冲区并重新开始。在服务器的特殊情况下,如果没有传入数据包可以读取,则通过 server::Handle
发送的非请求消息将被处理。
依赖项
~26MB
~259K SLoC