This commit is contained in:
nora 2024-08-25 18:00:43 +02:00
parent 1c346659f6
commit 8114b5a195
9 changed files with 433 additions and 21 deletions

36
Cargo.lock generated
View file

@ -433,6 +433,11 @@ dependencies = [
"cluelessh-protocol", "cluelessh-protocol",
"cluelessh-tokio", "cluelessh-tokio",
"cluelessh-transport", "cluelessh-transport",
"eyre",
"rustix",
"tokio",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
@ -598,6 +603,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "errno"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "eyre" name = "eyre"
version = "0.6.12" version = "0.6.12"
@ -843,6 +858,12 @@ version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "linux-raw-sys"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.12" version = "0.4.12"
@ -1205,6 +1226,21 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags",
"errno",
"itoa",
"libc",
"linux-raw-sys",
"once_cell",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"

View file

@ -47,6 +47,14 @@ async fn main() -> eyre::Result<()> {
}) })
})), })),
verify_pubkey: None, verify_pubkey: None,
auth_banner: Some(
"\
!! this system ONLY allows catgirls to enter !!\r\n\
!! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n\
!! THIS SYTEM WILL LOG AND STORE YOUR CLEARTEXT PASSWORD !!\r\n\
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n"
.to_owned(),
),
}; };
let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify); let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify);

View file

@ -136,7 +136,7 @@ async fn main() -> eyre::Result<()> {
} }
async fn main_channel(channel: PendingChannel) -> Result<()> { async fn main_channel(channel: PendingChannel) -> Result<()> {
let Ok(mut channel) = channel.wait_ready().await else { let Ok(channel) = channel.wait_ready().await else {
bail!("failed to create channel"); bail!("failed to create channel");
}; };

View file

@ -7,6 +7,11 @@ edition = "2021"
cluelessh-protocol = { path = "../../lib/cluelessh-protocol" } cluelessh-protocol = { path = "../../lib/cluelessh-protocol" }
cluelessh-tokio = { path = "../../lib/cluelessh-tokio" } cluelessh-tokio = { path = "../../lib/cluelessh-tokio" }
cluelessh-transport = { path = "../../lib/cluelessh-transport" } cluelessh-transport = { path = "../../lib/cluelessh-transport" }
tokio = { version = "1.39.2", features = ["full"] }
tracing.workspace = true
eyre.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
rustix = { version = "0.38.34", features = ["pty", "termios", "procfs", "process", "stdio"] }
[lints] [lints]
workspace = true workspace = true

View file

@ -1,3 +1,249 @@
fn main() { mod pty;
println!("Hello, world!");
use std::{io, net::SocketAddr, process::ExitStatus, sync::Arc};
use cluelessh_tokio::{server::ServerAuthVerify, Channel};
use eyre::{bail, Context, Result};
use pty::Pty;
use rustix::termios::Winsize;
use tokio::{
net::{TcpListener, TcpStream},
process::Command,
sync::mpsc,
};
use tracing::{debug, error, info, info_span, warn, Instrument};
use cluelessh_protocol::{
connection::{ChannelKind, ChannelOperationKind, ChannelRequest},
ChannelUpdateKind, SshStatus,
};
use tracing_subscriber::EnvFilter;
#[tokio::main(flavor = "current_thread")]
async fn main() -> eyre::Result<()> {
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
let addr = "0.0.0.0:2222".to_owned();
let addr = addr
.parse::<SocketAddr>()
.wrap_err_with(|| format!("failed to parse listen addr '{addr}'"))?;
info!(%addr, "Starting server");
let listener = TcpListener::bind(addr).await.wrap_err("binding listener")?;
let auth_verify = ServerAuthVerify {
verify_password: Some(Arc::new(|auth| {
Box::pin(async move {
debug!(user = %auth.user, "Attempting password login");
// Don't worry queen, your password is correct!
warn!("Letting in unauthenticated user");
Ok(())
})
})),
verify_pubkey: None,
auth_banner: Some("welcome to my server!!!\r\ni hope you enjoy our stay.\r\n".to_owned()),
};
let mut listener = cluelessh_tokio::server::ServerListener::new(listener, auth_verify);
loop {
let next = listener.accept().await?;
let span = info_span!("connection", addr = %next.peer_addr());
tokio::spawn(
async move {
if let Err(err) = handle_connection(next).await {
if let Some(err) = err.downcast_ref::<std::io::Error>() {
if err.kind() == std::io::ErrorKind::ConnectionReset {
return;
}
}
error!(?err, "error handling connection");
}
info!("Finished connection");
}
.instrument(span),
);
}
}
async fn handle_connection(
mut conn: cluelessh_tokio::server::ServerConnection<TcpStream>,
) -> Result<()> {
info!(addr = %conn.peer_addr(), "Received a new connection");
loop {
match conn.progress().await {
Ok(()) => {}
Err(cluelessh_tokio::server::Error::ServerError(err)) => {
return Err(err);
}
Err(cluelessh_tokio::server::Error::SshStatus(status)) => match status {
SshStatus::PeerError(err) => {
info!(?err, "disconnecting client after invalid operation");
return Ok(());
}
SshStatus::Disconnect => {
info!("Received disconnect from client");
return Ok(());
}
},
}
while let Some(channel) = conn.next_new_channel() {
if *channel.kind() == ChannelKind::Session {
tokio::spawn(async {
let _ = handle_session_channel(channel).await;
});
} else {
warn!("Trying to open non-session channel");
}
}
}
}
struct SessionState {
pty: Option<Pty>,
channel: Channel,
process_exit_send: mpsc::Sender<Result<ExitStatus, io::Error>>,
process_exit_recv: mpsc::Receiver<Result<ExitStatus, io::Error>>,
}
async fn handle_session_channel(channel: Channel) -> Result<()> {
let (process_exit_send, process_exit_recv) = tokio::sync::mpsc::channel(1);
let mut state = SessionState {
pty: None,
channel,
process_exit_send,
process_exit_recv,
};
loop {
let pty_read = async {
match &mut state.pty {
Some(pty) => pty.ctrl_read_recv.recv().await,
// Ensure that if this is None, the future never finishes so the state update and process exit can progress.
None => loop {
tokio::task::yield_now().await;
},
}
};
tokio::select! {
update = state.channel.next_update() => {
match update {
Ok(update) => state.handle_channel_update(update).await?,
Err(err) => return Err(err),
}
}
exit = state.process_exit_recv.recv() => {
match exit {
Some(exit) => {
let exit = exit?;
state.channel.send(ChannelOperationKind::Eof).await?;
// TODO: also handle exit-signal
state.channel
.send(ChannelOperationKind::Request(ChannelRequest::ExitStatus {
status: exit.code().unwrap_or(0) as u32,
}))
.await?;
state.channel.send(ChannelOperationKind::Close).await?;
return Ok(());
}
None => {}
}
}
read = pty_read => {
let Some(read) = read else {
bail!("failed to read");
};
let _ = state.channel.send(ChannelOperationKind::Data(read)).await;
}
}
}
}
impl SessionState {
async fn handle_channel_update(&mut self, update: ChannelUpdateKind) -> Result<()> {
match update {
ChannelUpdateKind::Request(req) => {
let success = ChannelOperationKind::Success;
match req {
ChannelRequest::PtyReq {
want_reply,
term,
width_chars,
height_rows,
width_px,
height_px,
term_modes,
} => {
self.pty = Some(
pty::Pty::new(
term,
Winsize {
ws_row: height_rows as u16,
ws_col: width_chars as u16,
ws_xpixel: width_px as u16,
ws_ypixel: height_px as u16,
},
term_modes,
)
.await?,
);
if want_reply {
self.channel.send(success).await?;
}
}
ChannelRequest::Shell { want_reply } => {
let shell = "bash";
let mut cmd = Command::new(shell);
cmd.env_clear();
if let Some(pty) = &self.pty {
pty.start_session_for_command(&mut cmd)?;
}
// TODO: **user** home directory
cmd.current_dir(std::env::var("HOME").unwrap_or_else(|_| "/".to_owned()));
cmd.env("USER", "nora");
let mut shell = cmd.spawn()?;
let process_exit_send = self.process_exit_send.clone();
tokio::spawn(async move {
let result = shell.wait().await;
let _ = process_exit_send.send(result).await;
});
if want_reply {
self.channel.send(success).await?;
}
}
ChannelRequest::Exec { .. } => {
todo!()
}
ChannelRequest::ExitStatus { .. } => {}
ChannelRequest::Env { .. } => {}
};
}
ChannelUpdateKind::OpenFailed { .. } => todo!(),
ChannelUpdateKind::Data { data } => match &mut self.pty {
Some(pty) => {
pty.ctrl_write_send.send(data).await?;
}
None => {}
},
ChannelUpdateKind::Open(_)
| ChannelUpdateKind::Closed
| ChannelUpdateKind::ExtendedData { .. }
| ChannelUpdateKind::Eof
| ChannelUpdateKind::Success
| ChannelUpdateKind::Failure => { /* ignore */ }
}
Ok(())
}
} }

113
bin/cluelesshd/src/pty.rs Normal file
View file

@ -0,0 +1,113 @@
//! PTY-related operations for setting up the session.
use std::{
io::{Read, Write},
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
path::PathBuf,
};
use eyre::{Context, Result};
use rustix::{
fs::{Mode, OFlags},
pty::OpenptFlags,
termios::Winsize,
};
use tokio::{process::Command, sync::mpsc, task::JoinHandle};
pub struct Pty {
term: String,
#[expect(dead_code)]
writer_handle: JoinHandle<()>,
#[expect(dead_code)]
reader_handle: JoinHandle<()>,
pub ctrl_write_send: mpsc::Sender<Vec<u8>>,
pub ctrl_read_recv: mpsc::Receiver<Vec<u8>>,
user_pty: OwnedFd,
}
impl Pty {
pub async fn new(term: String, winsize: Winsize, modes: Vec<u8>) -> Result<Self> {
tokio::task::spawn_blocking(move || Self::new_blocking(term, winsize, modes)).await?
}
pub fn new_blocking(term: String, winsize: Winsize, modes: Vec<u8>) -> Result<Self> {
// Create new PTY:
let controller = rustix::pty::openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY)
.wrap_err("opening controller pty")?;
rustix::pty::unlockpt(&controller).wrap_err("unlocking pty")?;
let user_pty_name = rustix::pty::ptsname(&controller, Vec::new())?;
let user_pty_name =
std::str::from_utf8(user_pty_name.as_bytes()).wrap_err("pty name is invalid UTF-8")?;
let user_pty_name = PathBuf::from(user_pty_name);
let user_pty =
rustix::fs::open(user_pty_name, OFlags::RDWR | OFlags::NOCTTY, Mode::empty())?;
// Configure terminal:
rustix::termios::tcsetwinsize(&user_pty, winsize)?;
let termios = rustix::termios::tcgetattr(&user_pty)?;
// TODO: set modes
// <https://datatracker.ietf.org/doc/html/rfc4254#section-8>
let _ = termios;
let _ = modes;
rustix::termios::tcsetattr(&user_pty, rustix::termios::OptionalActions::Flush, &termios)?;
// Set up communication threads:
let mut controller_read = std::fs::File::from(controller);
let mut controller_write = controller_read.try_clone()?;
let (ctrl_write_send, mut ctrl_write_recv) = tokio::sync::mpsc::channel::<Vec<u8>>(10);
let (ctrl_read_send, ctrl_read_recv) = tokio::sync::mpsc::channel::<Vec<u8>>(10);
let writer_handle = tokio::task::spawn_blocking(move || {
while let Some(write) = ctrl_write_recv.blocking_recv() {
let _ = controller_write.write_all(&write);
}
});
let reader_handle = tokio::task::spawn_blocking(move || {
let mut buf = [0; 1024];
loop {
let Ok(read) = controller_read.read(&mut buf) else {
return;
};
let Ok(_) = ctrl_read_send.blocking_send(buf[..read].to_vec()) else {
return;
};
}
});
Ok(Self {
term,
writer_handle,
reader_handle,
ctrl_write_send,
ctrl_read_recv,
user_pty,
})
}
pub fn start_session_for_command(&self, cmd: &mut Command) -> Result<()> {
let user_pty = self.user_pty.as_raw_fd();
unsafe {
cmd.pre_exec(move || {
let user_pty = BorrowedFd::borrow_raw(user_pty);
rustix::pty::grantpt(user_pty)?;
let pid = rustix::process::setsid()?;
rustix::process::ioctl_tiocsctty(user_pty)?; // Set as the current controlling tty
rustix::termios::tcsetpgrp(user_pty, pid)?; // Set current process as tty controller
// Setup stdio with PTY.
rustix::stdio::dup2_stdin(user_pty)?;
rustix::stdio::dup2_stdout(user_pty)?;
rustix::stdio::dup2_stderr(user_pty)?;
Ok(())
});
cmd.env("TERM", &self.term);
}
Ok(())
}
}

View file

@ -24,7 +24,7 @@ pub struct ServerConnection {
} }
enum ServerConnectionState { enum ServerConnectionState {
Setup(HashSet<AuthOption>), Setup(HashSet<AuthOption>, Option<String>),
Auth(auth::ServerAuth), Auth(auth::ServerAuth),
Open(cluelessh_connection::ChannelsState), Open(cluelessh_connection::ChannelsState),
} }
@ -33,20 +33,22 @@ impl ServerConnection {
pub fn new( pub fn new(
transport: cluelessh_transport::server::ServerConnection, transport: cluelessh_transport::server::ServerConnection,
auth_options: HashSet<AuthOption>, auth_options: HashSet<AuthOption>,
auth_banner: Option<String>,
) -> Self { ) -> Self {
Self { Self {
transport, transport,
state: ServerConnectionState::Setup(auth_options), state: ServerConnectionState::Setup(auth_options, auth_banner),
} }
} }
pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> { pub fn recv_bytes(&mut self, bytes: &[u8]) -> Result<()> {
self.transport.recv_bytes(bytes)?; self.transport.recv_bytes(bytes)?;
if let ServerConnectionState::Setup(options) = &mut self.state { if let ServerConnectionState::Setup(options, auth_banner) = &mut self.state {
if let Some(session_ident) = self.transport.is_open() { if let Some(session_ident) = self.transport.is_open() {
self.state = ServerConnectionState::Auth(auth::ServerAuth::new( self.state = ServerConnectionState::Auth(auth::ServerAuth::new(
mem::take(options), mem::take(options),
auth_banner.take(),
session_ident, session_ident,
)); ));
} }
@ -54,7 +56,7 @@ impl ServerConnection {
while let Some(packet) = self.transport.next_plaintext_packet() { while let Some(packet) = self.transport.next_plaintext_packet() {
match &mut self.state { match &mut self.state {
ServerConnectionState::Setup(_) => unreachable!(), ServerConnectionState::Setup(_, _) => unreachable!(),
ServerConnectionState::Auth(auth) => { ServerConnectionState::Auth(auth) => {
auth.recv_packet(packet)?; auth.recv_packet(packet)?;
for to_send in auth.packets_to_send() { for to_send in auth.packets_to_send() {
@ -78,14 +80,14 @@ impl ServerConnection {
pub fn next_channel_update(&mut self) -> Option<cluelessh_connection::ChannelUpdate> { pub fn next_channel_update(&mut self) -> Option<cluelessh_connection::ChannelUpdate> {
match &mut self.state { match &mut self.state {
ServerConnectionState::Setup(_) | ServerConnectionState::Auth(_) => None, ServerConnectionState::Setup(..) | ServerConnectionState::Auth(_) => None,
ServerConnectionState::Open(con) => con.next_channel_update(), ServerConnectionState::Open(con) => con.next_channel_update(),
} }
} }
pub fn do_operation(&mut self, op: ChannelOperation) { pub fn do_operation(&mut self, op: ChannelOperation) {
match &mut self.state { match &mut self.state {
ServerConnectionState::Setup(_) | ServerConnectionState::Auth(_) => { ServerConnectionState::Setup(..) | ServerConnectionState::Auth(_) => {
panic!("tried to get connection before it is ready") panic!("tried to get connection before it is ready")
} }
ServerConnectionState::Open(con) => { ServerConnectionState::Open(con) => {
@ -97,7 +99,7 @@ impl ServerConnection {
pub fn progress(&mut self) { pub fn progress(&mut self) {
match &mut self.state { match &mut self.state {
ServerConnectionState::Setup(_) => {} ServerConnectionState::Setup(..) => {}
ServerConnectionState::Auth(auth) => { ServerConnectionState::Auth(auth) => {
for to_send in auth.packets_to_send() { for to_send in auth.packets_to_send() {
self.transport.send_plaintext_packet(to_send); self.transport.send_plaintext_packet(to_send);
@ -263,7 +265,7 @@ pub mod auth {
packets_to_send: VecDeque<Packet>, packets_to_send: VecDeque<Packet>,
is_authenticated: bool, is_authenticated: bool,
options: HashSet<AuthOption>, options: HashSet<AuthOption>,
banner: Option<String>,
server_requests: VecDeque<ServerRequest>, server_requests: VecDeque<ServerRequest>,
session_ident: [u8; 32], session_ident: [u8; 32],
} }
@ -292,13 +294,18 @@ pub mod auth {
} }
impl ServerAuth { impl ServerAuth {
pub fn new(options: HashSet<AuthOption>, session_ident: [u8; 32]) -> Self { pub fn new(
options: HashSet<AuthOption>,
banner: Option<String>,
session_ident: [u8; 32],
) -> Self {
Self { Self {
has_failed: false, has_failed: false,
packets_to_send: VecDeque::new(), packets_to_send: VecDeque::new(),
options, options,
is_authenticated: false, is_authenticated: false,
session_ident, session_ident,
banner,
server_requests: VecDeque::new(), server_requests: VecDeque::new(),
} }
} }
@ -385,14 +392,9 @@ pub mod auth {
} }
_ => { _ => {
// Initial: // Initial:
self.queue_packet(Packet::new_msg_userauth_banner( if let Some(banner) = &self.banner {
b"!! this system ONLY allows catgirls to enter !!\r\n\ self.queue_packet(Packet::new_msg_userauth_banner(banner.as_bytes(), b""));
!! all other attempts WILL be prosecuted to the full extent of the rawr !!\r\n\ }
!! THIS SYTEM WILL LOG AND STORE YOUR CLEARTEXT PASSWORD !!\r\n\
!! DO NOT ENTER PASSWORDS YOU DON'T WANT STOLEN !!\r\n",
b"",
));
self.send_failure(); self.send_failure();
// Stay in the same state // Stay in the same state
} }

View file

@ -13,7 +13,7 @@ pub struct Channel {
} }
impl Channel { impl Channel {
pub async fn send(&mut self, op: ChannelOperationKind) -> Result<()> { pub async fn send(&self, op: ChannelOperationKind) -> Result<()> {
self.ops_send self.ops_send
.send(self.number.construct_op(op)) .send(self.number.construct_op(op))
.await .await

View file

@ -59,6 +59,7 @@ pub struct ServerAuthVerify {
Option<Arc<dyn Fn(VerifyPassword) -> BoxFuture<'static, Result<()>> + Send + Sync>>, Option<Arc<dyn Fn(VerifyPassword) -> BoxFuture<'static, Result<()>> + Send + Sync>>,
pub verify_pubkey: pub verify_pubkey:
Option<Arc<dyn Fn(VerifyPubkey) -> BoxFuture<'static, Result<()>> + Send + Sync>>, Option<Arc<dyn Fn(VerifyPubkey) -> BoxFuture<'static, Result<()>> + Send + Sync>>,
pub auth_banner: Option<String>,
} }
fn _assert_send_sync() { fn _assert_send_sync() {
fn send<T: Send + Sync>() {} fn send<T: Send + Sync>() {}
@ -125,6 +126,7 @@ impl<S: AsyncRead + AsyncWrite> ServerConnection<S> {
cluelessh_protocol::ThreadRngRand, cluelessh_protocol::ThreadRngRand,
), ),
options, options,
auth_verify.auth_banner.clone(),
), ),
new_channels: VecDeque::new(), new_channels: VecDeque::new(),
auth_verify, auth_verify,