From 0efd08dd5c2b1dd3ca85decc49e85ffc2c64e63b Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Sun, 11 Aug 2024 23:38:35 +0200 Subject: [PATCH] refactor --- Cargo.lock | 19 ++- Cargo.toml | 4 +- src/main.rs | 9 +- ssh-connection/Cargo.toml | 8 + ssh-connection/README.md | 5 + .../channel.rs => ssh-connection/src/lib.rs | 65 ++++--- ssh-protocol/Cargo.toml | 9 + ssh-protocol/README.md | 5 + ssh-protocol/src/lib.rs | 160 ++++++++++++++++++ ssh-transport/README.md | 4 - ssh-transport/src/lib.rs | 129 ++------------ ssh-transport/src/parse.rs | 6 +- 12 files changed, 268 insertions(+), 155 deletions(-) create mode 100644 ssh-connection/Cargo.toml create mode 100644 ssh-connection/README.md rename ssh-transport/src/channel.rs => ssh-connection/src/lib.rs (78%) create mode 100644 ssh-protocol/Cargo.toml create mode 100644 ssh-protocol/README.md create mode 100644 ssh-protocol/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b7dfb24..3816320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,7 +225,7 @@ version = "0.1.0" dependencies = [ "eyre", "hex-literal", - "ssh-transport", + "ssh-protocol", "tokio", "tracing", "tracing-subscriber", @@ -663,6 +663,23 @@ dependencies = [ "der", ] +[[package]] +name = "ssh-connection" +version = "0.1.0" +dependencies = [ + "ssh-transport", + "tracing", +] + +[[package]] +name = "ssh-protocol" +version = "0.1.0" +dependencies = [ + "ssh-connection", + "ssh-transport", + "tracing", +] + [[package]] name = "ssh-transport" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f534fae..8f516ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "ssh-transport"] +members = ["ssh-connection", "ssh-protocol", "ssh-transport"] [package] name = "fakessh" @@ -9,7 +9,7 @@ edition = "2021" [dependencies] eyre = "0.6.12" hex-literal = "0.4.1" -ssh-transport = { path = "./ssh-transport" } +ssh-protocol = { path = "./ssh-protocol" } tokio = { version = "1.39.2", features = ["full"] } tracing = "0.1.40" diff --git a/src/main.rs b/src/main.rs index 88af154..d17db52 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,10 @@ use tokio::{ }; use tracing::{error, info}; -use ssh_transport::{ServerConnection, SshStatus, ThreadRngRand}; +use ssh_protocol::{ + transport::{self, ThreadRngRand}, + ServerConnection, SshStatus, +}; use tracing_subscriber::EnvFilter; #[tokio::main] @@ -53,7 +56,7 @@ async fn handle_connection(next: (TcpStream, SocketAddr)) -> Result<()> { // } //} - let mut state = ServerConnection::new(ThreadRngRand); + let mut state = ServerConnection::new(transport::ServerConnection::new(ThreadRngRand)); loop { let mut buf = [0; 1024]; @@ -80,8 +83,6 @@ async fn handle_connection(next: (TcpStream, SocketAddr)) -> Result<()> { } } - while let Some(channel_update) = state.next_channel_update() {} - while let Some(msg) = state.next_msg_to_send() { conn.write_all(&msg.to_bytes()) .await diff --git a/ssh-connection/Cargo.toml b/ssh-connection/Cargo.toml new file mode 100644 index 0000000..d49b91c --- /dev/null +++ b/ssh-connection/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "ssh-connection" +version = "0.1.0" +edition = "2021" + +[dependencies] +ssh-transport = { path = "../ssh-transport" } +tracing = "0.1.40" diff --git a/ssh-connection/README.md b/ssh-connection/README.md new file mode 100644 index 0000000..cbff87c --- /dev/null +++ b/ssh-connection/README.md @@ -0,0 +1,5 @@ +# ssh-connection + +Connection layer for SSH. This crate takes care of channel multiplexing. + +Based on [RFC 4254 The Secure Shell (SSH) Connection Protocol](https://datatracker.ietf.org/doc/html/rfc4254). diff --git a/ssh-transport/src/channel.rs b/ssh-connection/src/lib.rs similarity index 78% rename from ssh-transport/src/channel.rs rename to ssh-connection/src/lib.rs index c40c33c..60a0cae 100644 --- a/ssh-transport/src/channel.rs +++ b/ssh-connection/src/lib.rs @@ -1,12 +1,11 @@ use std::collections::VecDeque; use tracing::{debug, warn}; -use crate::client_error; -use crate::packet::Packet; -use crate::parse::{Parser, Writer}; -use crate::Result; +use ssh_transport::client_error; +use ssh_transport::packet::Packet; +use ssh_transport::Result; -pub(crate) struct ServerChannelsState { +pub struct ServerChannelsState { packets_to_send: VecDeque, channels: Vec, @@ -26,13 +25,17 @@ pub struct ChannelUpdate { pub channel: u32, pub kind: ChannelUpdateKind, } - pub enum ChannelUpdateKind { - ChannelData(Vec), + Create { kind: String, args: Vec }, + Request { kind: String, args: Vec }, + Data { data: Vec }, + ExtendedData { code: u32, data: Vec }, + Eof, + ChannelClosed, } impl ServerChannelsState { - pub(crate) fn new() -> Self { + pub fn new() -> Self { ServerChannelsState { packets_to_send: VecDeque::new(), channels: Vec::new(), @@ -40,14 +43,24 @@ impl ServerChannelsState { } } - pub(crate) fn on_packet(&mut self, packet_type: u8, mut payload: Parser<'_>) -> Result<()> { + pub fn recv_packet(&mut self, packet: Packet) -> Result<()> { + let mut packet = packet.payload_parser(); + let packet_type = packet.u8()?; match packet_type { + Packet::SSH_MSG_GLOBAL_REQUEST => { + let request_name = packet.utf8_string()?; + let want_reply = packet.bool()?; + debug!(?request_name, ?want_reply, "Received global request"); + + self.packets_to_send + .push_back(Packet::new_msg_request_failure()); + } Packet::SSH_MSG_CHANNEL_OPEN => { // - let channel_type = payload.utf8_string()?; - let sender_channel = payload.u32()?; - let initial_window_size = payload.u32()?; - let max_packet_size = payload.u32()?; + let channel_type = packet.utf8_string()?; + let sender_channel = packet.u32()?; + let initial_window_size = packet.u32()?; + let max_packet_size = packet.u32()?; debug!(?channel_type, ?sender_channel, "Opening channel"); @@ -85,8 +98,8 @@ impl ServerChannelsState { } } Packet::SSH_MSG_CHANNEL_DATA => { - let our_channel = payload.u32()?; - let data = payload.string()?; + let our_channel = packet.u32()?; + let data = packet.string()?; let channel = self.channel(our_channel)?; channel.recv_bytes(data); @@ -108,7 +121,7 @@ impl ServerChannelsState { } Packet::SSH_MSG_CHANNEL_CLOSE => { // - let our_channel = payload.u32()?; + let our_channel = packet.u32()?; let channel = self.channel(our_channel)?; if !channel.we_closed { let close = Packet::new_msg_channel_close(channel.peer_channel); @@ -120,9 +133,9 @@ impl ServerChannelsState { debug!("Channel has been closed"); } Packet::SSH_MSG_CHANNEL_REQUEST => { - let our_channel = payload.u32()?; - let request_type = payload.utf8_string()?; - let want_reply = payload.bool()?; + let our_channel = packet.u32()?; + let request_type = packet.utf8_string()?; + let want_reply = packet.bool()?; debug!(?our_channel, ?request_type, "Got channel request"); @@ -131,12 +144,12 @@ impl ServerChannelsState { match request_type { "pty-req" => { - let term = payload.utf8_string()?; - let width_chars = payload.u32()?; - let height_rows = payload.u32()?; - let _width_px = payload.u32()?; - let _height_px = payload.u32()?; - let _term_modes = payload.string()?; + let term = packet.utf8_string()?; + let width_chars = packet.u32()?; + let height_rows = packet.u32()?; + let _width_px = packet.u32()?; + let _height_px = packet.u32()?; + let _term_modes = packet.string()?; debug!( ?our_channel, @@ -186,7 +199,7 @@ impl ServerChannelsState { Ok(()) } - pub(crate) fn packets_to_send(&mut self) -> impl Iterator + '_ { + pub fn packets_to_send(&mut self) -> impl Iterator + '_ { self.packets_to_send.drain(..) } diff --git a/ssh-protocol/Cargo.toml b/ssh-protocol/Cargo.toml new file mode 100644 index 0000000..2b99e9a --- /dev/null +++ b/ssh-protocol/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ssh-protocol" +version = "0.1.0" +edition = "2021" + +[dependencies] +ssh-connection = { path = "../ssh-connection" } +ssh-transport = { path = "../ssh-transport" } +tracing = "0.1.40" diff --git a/ssh-protocol/README.md b/ssh-protocol/README.md new file mode 100644 index 0000000..b1fdd39 --- /dev/null +++ b/ssh-protocol/README.md @@ -0,0 +1,5 @@ +# ssh-protocol + +Combines `ssh-connection` and `ssh-transport` into a higher level interface. + +Also implements authentication based on [RFC 4252 The Secure Shell (SSH) Authentication Protocol](https://datatracker.ietf.org/doc/html/rfc4252). diff --git a/ssh-protocol/src/lib.rs b/ssh-protocol/src/lib.rs new file mode 100644 index 0000000..3aae6b8 --- /dev/null +++ b/ssh-protocol/src/lib.rs @@ -0,0 +1,160 @@ +pub use ssh_transport as transport; +pub use ssh_transport::{Result, SshStatus}; + +pub struct ServerConnection { + transport: ssh_transport::ServerConnection, + state: ServerConnectionState, +} + +enum ServerConnectionState { + Auth(auth::BadAuth), + Open(ssh_connection::ServerChannelsState), +} + +impl ServerConnection { + pub fn new(transport: ssh_transport::ServerConnection) -> Self { + Self { + transport, + state: ServerConnectionState::Auth(auth::BadAuth::new()), + } + } + + pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> { + self.transport.recv_bytes(bytes)?; + + while let Some(packet) = self.transport.next_plaintext_packet() { + match &mut self.state { + ServerConnectionState::Auth(auth) => { + auth.recv_packet(packet)?; + for to_send in auth.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + if auth.is_authenticated() { + self.state = ServerConnectionState::Open(ssh_connection::ServerChannelsState::new()); + } + } + ServerConnectionState::Open(con) => { + con.recv_packet(packet)?; + for to_send in con.packets_to_send() { + self.transport.send_plaintext_packet(to_send); + } + } + } + } + + Ok(()) + } + + pub fn next_msg_to_send(&mut self) -> Option { + self.transport.next_msg_to_send() + } +} + +/// +pub mod auth { + use std::collections::VecDeque; + + use ssh_transport::{client_error, packet::Packet, parse::NameList, Result}; + use tracing::info; + + pub struct BadAuth { + has_failed: bool, + packets_to_send: VecDeque, + is_authenticated: bool, + } + + impl BadAuth { + pub fn new() -> Self { + Self { + has_failed: false, + packets_to_send: VecDeque::new(), + is_authenticated: false, + } + } + + pub fn recv_packet(&mut self, packet: Packet) -> Result<()> { + assert!(!self.is_authenticated, "Must not feed more packets to authentication after authentication is been completed, check with .is_authenticated()"); + + // This is a super simplistic implementation of RFC4252 SSH authentication. + // We ask for a public key, and always let that one pass. + // The reason for this is that this makes it a lot easier to test locally. + // It's not very good, but it's good enough for now. + let mut auth_req = packet.payload_parser(); + + if auth_req.u8()? != Packet::SSH_MSG_USERAUTH_REQUEST { + return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST")); + } + let username = auth_req.utf8_string()?; + let service_name = auth_req.utf8_string()?; + let method_name = auth_req.utf8_string()?; + + info!( + ?username, + ?service_name, + ?method_name, + "User trying to authenticate" + ); + + if service_name != "ssh-connection" { + return Err(client_error!( + "client tried to unsupported service: {service_name}" + )); + } + + match method_name { + "password" => { + let change_password = auth_req.bool()?; + if change_password { + return Err(client_error!("client tried to change password unprompted")); + } + let password = auth_req.utf8_string()?; + + info!(?password, "Got password"); + // Don't worry queen, your password is correct! + self.queue_packet(Packet::new_msg_userauth_success()); + + self.is_authenticated = true; + } + "publickey" => { + info!("Got public key"); + // Don't worry queen, your key is correct! + self.queue_packet(Packet::new_msg_userauth_success()); + self.is_authenticated = true; + } + _ if self.has_failed => { + return Err(client_error!( + "client tried unsupported method twice: {method_name}" + )); + } + _ => { + // Initial. + + self.queue_packet(Packet::new_msg_userauth_banner( + b"!! this system ONLY allows catgirls to enter !!\r\n\ + !! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n", + b"", + )); + + self.queue_packet(Packet::new_msg_userauth_failure( + NameList::one("publickey"), + false, + )); + // Stay in the same state + } + } + Ok(()) + } + + pub fn packets_to_send(&mut self) -> impl Iterator + '_ { + self.packets_to_send.drain(..) + } + + pub fn is_authenticated(&self) -> bool { + self.is_authenticated + } + + fn queue_packet(&mut self, packet: Packet) { + self.packets_to_send.push_back(packet); + } + } +} diff --git a/ssh-transport/README.md b/ssh-transport/README.md index a321dd0..7d8983f 100644 --- a/ssh-transport/README.md +++ b/ssh-transport/README.md @@ -6,10 +6,6 @@ Based on [RFC 4253 The Secure Shell (SSH) Transport Layer Protocol](https://data and [RFC 4251 The Secure Shell (SSH) Protocol Architecture](https://datatracker.ietf.org/doc/html/rfc4251) and [RFC 4250 The Secure Shell (SSH) Protocol Assigned Numbers](https://datatracker.ietf.org/doc/html/rfc4250). -Also authentication and connection for now. -- [The Secure Shell (SSH) Authentication Protocol](https://datatracker.ietf.org/doc/html/rfc4252) -- [The Secure Shell (SSH) Connection Protocol](https://datatracker.ietf.org/doc/html/rfc4254) - Other relevant RFCs: - [RFC 5649 AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://datatracker.ietf.org/doc/html/rfc5647) - [RFC 5656 Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://datatracker.ietf.org/doc/html/rfc5656) diff --git a/ssh-transport/src/lib.rs b/ssh-transport/src/lib.rs index 1be2b3c..d80d3b5 100644 --- a/ssh-transport/src/lib.rs +++ b/ssh-transport/src/lib.rs @@ -1,4 +1,3 @@ -mod channel; mod keys; pub mod packet; pub mod parse; @@ -6,7 +5,6 @@ pub mod parse; use core::str; use std::{collections::VecDeque, mem::take}; -use channel::ServerChannelsState; use ed25519_dalek::ed25519::signature::Signer; use packet::{ DhKeyExchangeInitPacket, DhKeyExchangeInitReplyPacket, KeyExchangeInitPacket, Packet, @@ -18,7 +16,6 @@ use sha2::Digest; use tracing::{debug, info, trace}; use x25519_dalek::{EphemeralSecret, PublicKey}; -pub use channel::ChannelUpdate; pub use packet::Msg; #[derive(Debug)] @@ -52,7 +49,7 @@ pub struct ServerConnection { packet_transport: PacketTransport, rng: Box, - channel_updates: VecDeque, + plaintext_packets: VecDeque, } enum ServerState { @@ -72,14 +69,7 @@ enum ServerState { k: [u8; 32], }, ServiceRequest, - // At this point we transfer to - UserAuthRequest { - /// Whether the client has failed already (by sending the wrong method). - // The second failure results in disconnecting. - has_failed: bool, - }, - /// The connection has been opened, all connection-related messages are delegated to the connection handler. - ConnectionOpen(ServerChannelsState), + Open, } pub trait SshRng { @@ -121,7 +111,8 @@ impl ServerConnection { }, packet_transport: PacketTransport::new(), rng: Box::new(rng), - channel_updates: VecDeque::new(), + + plaintext_packets: VecDeque::new(), } } } @@ -343,6 +334,7 @@ impl ServerConnection { self.packet_transport.set_key(h, k); } ServerState::ServiceRequest => { + // TODO: this should probably move out of here? unsure. if packet.payload.first() != Some(&Packet::SSH_MSG_SERVICE_REQUEST) { return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST")); } @@ -362,107 +354,10 @@ impl ServerConnection { writer.finish() }, }); - self.state = ServerState::UserAuthRequest { has_failed: false }; + self.state = ServerState::Open; } - ServerState::UserAuthRequest { has_failed } => { - // This is a super simplistic implementation of RFC4252 SSH authentication. - // We ask for a public key, and always let that one pass. - // The reason for this is that this makes it a lot easier to test locally. - // It's not very good, but it's good enough for now. - let mut auth_req = packet.payload_parser(); - - if auth_req.u8()? != Packet::SSH_MSG_USERAUTH_REQUEST { - return Err(client_error!("did not send SSH_MSG_SERVICE_REQUEST")); - } - let username = auth_req.utf8_string()?; - let service_name = auth_req.utf8_string()?; - let method_name = auth_req.utf8_string()?; - - info!( - ?username, - ?service_name, - ?method_name, - "User trying to authenticate" - ); - - if service_name != "ssh-connection" { - return Err(client_error!( - "client tried to unsupported service: {service_name}" - )); - } - - match method_name { - "password" => { - let change_password = auth_req.bool()?; - if change_password { - return Err(client_error!( - "client tried to change password unprompted" - )); - } - let password = auth_req.utf8_string()?; - - info!(?password, "Got password"); - // Don't worry queen, your password is correct! - self.packet_transport - .queue_packet(Packet::new_msg_userauth_success()); - - self.state = ServerState::ConnectionOpen(ServerChannelsState::new()); - } - "publickey" => { - info!("Got public key"); - // Don't worry queen, your key is correct! - self.packet_transport - .queue_packet(Packet::new_msg_userauth_success()); - self.state = ServerState::ConnectionOpen(ServerChannelsState::new()); - } - _ if *has_failed => { - return Err(client_error!( - "client tried unsupported method twice: {method_name}" - )); - } - _ => { - // Initial. - - self.packet_transport.queue_packet(Packet::new_msg_userauth_banner( - b"!! this system ONLY allows catgirls to enter !!\r\n\ - !! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n", - b"", - )); - - self.packet_transport - .queue_packet(Packet::new_msg_userauth_failure( - NameList::one("publickey"), - false, - )); - // Stay in the same state - } - } - } - ServerState::ConnectionOpen(con) => { - let mut payload = packet.payload_parser(); - let packet_type = payload.u8()?; - - match packet_type { - // Connection-related packets - 90..128 => { - con.on_packet(packet_type, payload)?; - for packet in con.packets_to_send() { - self.packet_transport.queue_packet(packet); - } - self.channel_updates.extend(con.channel_updates()); - } - Packet::SSH_MSG_GLOBAL_REQUEST => { - let request_name = payload.utf8_string()?; - let want_reply = payload.bool()?; - debug!(?request_name, ?want_reply, "Received global request"); - - self.packet_transport - .queue_packet(Packet::new_msg_request_failure()); - } - _ => { - todo!("packet: {packet_type}"); - } - } + ServerState::Open => { + self.plaintext_packets.push_back(packet); } } } @@ -473,8 +368,12 @@ impl ServerConnection { self.packet_transport.next_msg_to_send() } - pub fn next_channel_update(&mut self) -> Option { - self.channel_updates.pop_front() + pub fn next_plaintext_packet(&mut self) -> Option { + self.plaintext_packets.pop_front() + } + + pub fn send_plaintext_packet(&mut self, packet: Packet) { + self.packet_transport.queue_packet(packet); } } diff --git a/ssh-transport/src/parse.rs b/ssh-transport/src/parse.rs index de27f36..6bdb008 100644 --- a/ssh-transport/src/parse.rs +++ b/ssh-transport/src/parse.rs @@ -119,16 +119,16 @@ impl Writer { pub struct NameList<'a>(&'a str); impl<'a> NameList<'a> { - pub(crate) fn one(item: &'a str) -> Self { + pub fn one(item: &'a str) -> Self { if item.contains(',') { //panic!("tried creating name list with comma in item: {item}"); } Self(item) } - pub(crate) fn none() -> NameList<'static> { + pub fn none() -> NameList<'static> { NameList("") } - pub(crate) fn iter(&self) -> std::str::Split { + pub fn iter(&self) -> std::str::Split { self.0.split(',') } }