Rust实现最简版的SSH通讯

源起

SSH几乎每天都用,也知道是安全的、加密的、可以远程操作shell的。随着对安全的越来越重视,突然有一天有了疑问,这是如何做到的?到底是否真的安全?因此,开启了一段SSH学习之旅。

目标

  1. 学习/理解/实践SSH协议(原理)
  2. 验证理解的内容:根据SSH协议的理解,实现一个最简版的ssh通讯机制

SSH协议简要理解

通过搜索阅读网络上一些文章,SSH协议基于TCP协议,主要分为三个部分:

  • 密钥协商(包含服务端指纹验证)
  • 身份验证
  • 数据加密传输

另外ssh协议中还有一些涉及到shell的操作,协议中并没有单独区分,此处单独区分,只是本人觉得SSH做为数据加密传输的协议来讲,这些shell的设计可以单独列出来。当然,我也搜索了一下SSH协议中为什么会有shell的操作,这是因为SSH协议的设计目的就是为了替代telnet之类的明文远程操作工具,因此有shell相关的定义就不可避免,所以为什么SSH是S-SH了

整体代码时序图

代码时序图

russh组件介绍

Server and client SSH asynchronous library, based on tokio/futures

组件文档地址

russh组件中客户端和服务端的相关的分别在russh::client和russh::server模块中。
代码中主要用到的模块和结构如下

命名空间 类型 名称 描述
russh:client struct Handle Handle to a session, used to send messages to a client outside of the request/response cycle.
russh:client struct Session Actual client session’s state.
russh::client trait Handler A client handler. Note that messages can be received from the server at any time during a session.
russh::client function connect/connect_stream Connect to a server at the address specified
russh::server struct Handle Handle to a session, used to send messages to a client outside of the request/response cycle.
russh::server struct Session A connected server session. This type is unique to a client.
russh::server trait Handler Server handler. Each client will have their own handler.
russh::server trait Server Trait used to create new handlers when clients connect.

代码实现

服务端

use std::{io::Write, sync::Arc};

use anyhow::Result;
use clap::Parser;
use env_logger;
use log::info;
use rand_core::OsRng;
use russh::{
    CryptoVec,
    server::{Auth, Config, Handler, Server},
};

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::builder()
        .filter_level(log::LevelFilter::Debug)
        .init();

    let start_args = CliStartArgs::parse();
    let mut server = ServerImpl {};
    let config = Config {
        keys: vec![
            russh::keys::PrivateKey::random(&mut OsRng, russh::keys::Algorithm::Ed25519).unwrap(),
        ],
        ..Default::default()
    };
    server
        .run_on_address(Arc::new(config), ("0.0.0.0", start_args.port))
        .await?;
    Ok(())
}

#[derive(Debug, Clone)]
struct ServerImpl {}
//对每个请求进行处理
impl Handler for ServerImpl {
    type Error = russh::Error;
    async fn auth_password(
        &mut self,
        user: &str,
        password: &str,
    ) -> std::result::Result<russh::server::Auth, Self::Error> {
        //todo:待完善
        if user.eq("sa") && password.eq("pass01!") {
            Ok(Auth::Accept)
        } else {
            Ok(Auth::Reject {
                proceed_with_methods: None, //todo:可以继续完善
                partial_success: false,
            })
        }
    }
    async fn data(
        &mut self,
        channel: russh::ChannelId,
        data: &[u8],
        session: &mut russh::server::Session,
    ) -> std::result::Result<(), Self::Error> {
        let recv_str = String::from_utf8(data.to_vec()).expect("parse to string failed!");
        info!("received from client: {}", recv_str);
        let mut response_content = Vec::new();
        response_content.extend("received from client:".as_bytes());
        response_content.extend_from_slice(data);
        let mut response = CryptoVec::new();
        response.write_all(&response_content)?;
        session.data(channel, response)?;
        Ok(())
    }
    async fn channel_open_session(
        &mut self,
        _channel: russh::Channel<russh::server::Msg>,
        _session: &mut russh::server::Session,
    ) -> std::result::Result<bool, Self::Error> {
        Ok(true)
    }
}
impl russh::server::Server for ServerImpl {
    type Handler = Self;

    fn new_client(&mut self, _peer_addr: Option<std::net::SocketAddr>) -> Self::Handler {
        self.clone()
    }
}

#[derive(Parser)]
struct CliStartArgs {
    #[arg(long, short = 'p')]
    port: u16,
}

客户端

use anyhow::Result;
use clap::Parser;
use log::{error, info};
use russh::{
    ChannelMsg, CryptoVec,
    client::{self, Config},
};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<()> {
    env_logger::builder()
        .filter_level(log::LevelFilter::Debug)
        .init();
    let start_args = CliStartArgs::parse();
    let config = Arc::new(Config {
        ..Default::default()
    });
    let client = ClientHandler {};
    let mut handler = client::connect(config, (start_args.server, start_args.port), client).await?;
    let auth_res = handler.authenticate_password("sa", "pass01!").await?;
    info!("auth success {}", auth_res.success());
    if auth_res.success() {
        let mut channel = handler.channel_open_session().await?;
        let send_data = "abcdef";
        let res = handler.data(channel.id(), CryptoVec::from(send_data)).await;
        if let Err(err) = res {
            error!("{:?}", err);
        }
        match channel.wait().await {
            Some(msg) => {
                if let ChannelMsg::Data { data } = msg {
                    match String::from_utf8(data.to_vec()) {
                        Ok(str) => {
                            info!("received from server: {}", str);
                        }
                        Err(_) => {
                            error!("responsed vec to string failed!");
                        }
                    }
                }
            }
            _ => {}
        }
    } else {
        error!("auth failed!");
    }
    Ok(())
}

struct ClientHandler {}
impl russh::client::Handler for ClientHandler {
    type Error = russh::Error;
    async fn check_server_key(
        &mut self,
        _server_public_key: &russh::keys::ssh_key::PublicKey,
    ) -> Result<bool, Self::Error> {
        Ok(true)
    }
}

#[derive(Parser)]
struct CliStartArgs {
    #[arg(long, short = 's')]
    server: String,
    #[arg(long, short = 'p')]
    port: u16,
    //暂时只支持用户名和密码,且这两个是hard code的
}

项目依赖

[dependencies]
anyhow = "1.0.98"
clap = { version = "4.5.39", features = ["derive"] }
env_logger = "0.11.8"
log = "0.4.27"
rand = "0.9"
russh = "0.52.1"
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread","io-std"] }
rand_core = { version = "0.6", features = [ "std"] }
shell-escape = "0.1.5"
posted @ 2025-06-12 09:26  薄醉愁听花  阅读(135)  评论(0)    收藏  举报