mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
bootstrap SFTP subsystem
This commit is contained in:
parent
8de8204bc7
commit
a9e2edc572
15 changed files with 205 additions and 17 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
|
@ -422,6 +422,17 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cluelessh-sftp"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "cluelessh-sftp-proto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cluelessh-transport",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cluelessh-tokio"
|
||||
version = "0.1.0"
|
||||
|
|
@ -489,6 +500,15 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cluelesshd-sftp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eyre",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
|
|
|
|||
|
|
@ -230,6 +230,11 @@ async fn handle_session_channel(
|
|||
channel.send(ChannelOperationKind::Eof).await?;
|
||||
channel.send(ChannelOperationKind::Close).await?;
|
||||
}
|
||||
ChannelRequest::Subsystem { want_reply, .. } => {
|
||||
if want_reply {
|
||||
channel.send(ChannelOperationKind::Failure).await?;
|
||||
}
|
||||
}
|
||||
ChannelRequest::ExitStatus { .. } => {}
|
||||
ChannelRequest::Env { .. } => {}
|
||||
};
|
||||
|
|
|
|||
12
bin/cluelesshd-sftp-server/Cargo.toml
Normal file
12
bin/cluelesshd-sftp-server/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "cluelesshd-sftp-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
eyre.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
3
bin/cluelesshd-sftp-server/README.md
Normal file
3
bin/cluelesshd-sftp-server/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# cluelesshd-sftp-server
|
||||
|
||||
SFTP server for cluelesshd.
|
||||
17
bin/cluelesshd-sftp-server/src/main.rs
Normal file
17
bin/cluelesshd-sftp-server/src/main.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use eyre::Result;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let env_filter =
|
||||
EnvFilter::try_from_env("SFTP_LOG").unwrap_or_else(|_| EnvFilter::new("debug"));
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
|
||||
info!("mroooow!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
log_level = "info"
|
||||
|
||||
|
||||
[subsystem.sftp]
|
||||
path = "../../target/debug/cluelesshd-sftp-server"
|
||||
|
||||
[net]
|
||||
ip = "0.0.0.0"
|
||||
port = 2223
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
use eyre::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::Args;
|
||||
|
||||
// TODO: validate config and user nicer structs to consume it
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
|
|
@ -15,6 +18,8 @@ pub struct Config {
|
|||
pub net: NetConfig,
|
||||
pub auth: AuthConfig,
|
||||
pub security: SecurityConfig,
|
||||
#[serde(default)]
|
||||
pub subsystem: HashMap<String, SubsystemConfig>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
|
@ -52,8 +57,21 @@ pub struct SecurityConfig {
|
|||
pub experimental_seccomp: bool,
|
||||
}
|
||||
|
||||
/// Add arbitrary subsystems.
|
||||
/// # Subsystem Protocol
|
||||
/// Every subsystem process gets spawned in the home directory of the user, as the user.
|
||||
/// Several FDs are guaranteed to be open.
|
||||
/// - stdin (0): data from the client channel
|
||||
/// - stdout (1): data to the client channel
|
||||
/// - stderr (2): data to the client channel extended stderr (used for debugging)
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SubsystemConfig {
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn find(args: &Args) -> Result<Self> {
|
||||
pub fn load(args: &Args) -> Result<Self> {
|
||||
let path = std::env::var("CLUELESSHD_CONFIG")
|
||||
.map(PathBuf::from)
|
||||
.or(args.config.clone().ok_or(std::env::VarError::NotPresent))
|
||||
|
|
@ -63,8 +81,19 @@ impl Config {
|
|||
format!("failed to open config file '{}', refusing to start. you can change the config file path with the --config arg or the CLUELESSHD_CONFIG environment variable", path.display())
|
||||
})?;
|
||||
|
||||
toml::from_str(&content)
|
||||
.wrap_err_with(|| format!("invalid config file '{}'", path.display()))
|
||||
let mut config: Config = toml::from_str(&content)
|
||||
.wrap_err_with(|| format!("invalid config file '{}'", path.display()))?;
|
||||
|
||||
for sub in config.subsystem.values_mut() {
|
||||
sub.path = sub.path.canonicalize().wrap_err_with(|| {
|
||||
format!(
|
||||
"error canonicalizing subsystem path: {}",
|
||||
sub.path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -292,7 +292,7 @@ impl SessionState {
|
|||
}
|
||||
}
|
||||
}
|
||||
ChannelRequest::Shell { want_reply } => match self.shell(None).await {
|
||||
ChannelRequest::Shell { want_reply } => match self.shell(None, None).await {
|
||||
Ok(()) => {
|
||||
if want_reply {
|
||||
self.channel.send(ChannelOperationKind::Success).await?;
|
||||
|
|
@ -309,7 +309,7 @@ impl SessionState {
|
|||
want_reply,
|
||||
command,
|
||||
} => match String::from_utf8(command) {
|
||||
Ok(command) => match self.shell(Some(&command)).await {
|
||||
Ok(command) => match self.shell(Some(command), None).await {
|
||||
Ok(()) => {
|
||||
if want_reply {
|
||||
self.channel.send(ChannelOperationKind::Success).await?;
|
||||
|
|
@ -330,6 +330,21 @@ impl SessionState {
|
|||
}
|
||||
}
|
||||
},
|
||||
ChannelRequest::Subsystem { want_reply, name } => {
|
||||
match self.shell(None, Some(name)).await {
|
||||
Ok(()) => {
|
||||
if want_reply {
|
||||
self.channel.send(ChannelOperationKind::Success).await?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(%err, "Failed to spawn subsystem");
|
||||
if want_reply {
|
||||
self.channel.send(ChannelOperationKind::Failure).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ChannelRequest::Env {
|
||||
name,
|
||||
value,
|
||||
|
|
@ -394,11 +409,12 @@ impl SessionState {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn shell(&mut self, shell_command: Option<&str>) -> Result<()> {
|
||||
async fn shell(&mut self, shell_command: Option<String>, subsystem: Option<String>) -> Result<()> {
|
||||
let mut fds = self
|
||||
.rpc_client
|
||||
.shell(
|
||||
shell_command.map(ToOwned::to_owned),
|
||||
shell_command,
|
||||
subsystem,
|
||||
self.pty_term.clone(),
|
||||
self.envs.clone(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,10 +51,14 @@ fn main() -> eyre::Result<()> {
|
|||
// Initial setup
|
||||
let args = Args::parse();
|
||||
|
||||
let config = config::Config::find(&args)?;
|
||||
let config = config::Config::load(&args)?;
|
||||
|
||||
setup_tracing(&config);
|
||||
|
||||
for (name, system) in &config.subsystem {
|
||||
info!(%name, path = %system.path.display(), "Loaded subsystem");
|
||||
}
|
||||
|
||||
if !rustix::process::getuid().is_root() {
|
||||
warn!("Daemon not started as root. This disables several security mitigations and permits logging in as any other user");
|
||||
}
|
||||
|
|
@ -197,7 +201,8 @@ async fn spawn_connection_child(
|
|||
) -> Result<()> {
|
||||
let stream_fd = stream.as_raw_fd();
|
||||
|
||||
let mut rpc_server = rpc::Server::new(host_keys).wrap_err("creating RPC server")?;
|
||||
let mut rpc_server =
|
||||
rpc::Server::new(config.clone(), host_keys).wrap_err("creating RPC server")?;
|
||||
|
||||
let rpc_client_fd = rpc_server.client_fd().as_raw_fd();
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ use users::os::unix::UserExt;
|
|||
use users::User;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum Request {
|
||||
/// Performs the key exchange by generating a private key, deriving the shared secret,
|
||||
|
|
@ -136,6 +138,7 @@ struct ShellRequest {
|
|||
/// Whether a PTY is used and if yes, the TERM env var.
|
||||
pty_term: Option<String>,
|
||||
command: Option<String>,
|
||||
subsystem: Option<String>,
|
||||
env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
|
|
@ -162,17 +165,20 @@ pub struct Server {
|
|||
host_keys: Vec<PlaintextPrivateKey>,
|
||||
authenticated_user: Option<users::User>,
|
||||
|
||||
config: Config,
|
||||
|
||||
pty_user: Option<OwnedFd>,
|
||||
shell_process: Option<Child>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(host_keys: Vec<PlaintextPrivateKey>) -> Result<Self> {
|
||||
pub fn new(config: Config, host_keys: Vec<PlaintextPrivateKey>) -> Result<Self> {
|
||||
let (server, client) = UnixDatagram::pair().wrap_err("creating socketpair")?;
|
||||
|
||||
Ok(Self {
|
||||
server,
|
||||
client,
|
||||
config,
|
||||
host_keys,
|
||||
authenticated_user: None,
|
||||
pty_user: None,
|
||||
|
|
@ -366,13 +372,28 @@ impl Server {
|
|||
}
|
||||
|
||||
async fn shell(&mut self, user: &User, req: ShellRequest) -> Result<Vec<OwnedFd>> {
|
||||
let subsystem = match req.subsystem.as_deref() {
|
||||
Some(subsystem) => match self.config.subsystem.get(subsystem) {
|
||||
Some(system) => Some(system.path.clone()),
|
||||
None => bail!("unsupported subsystem: {subsystem}"),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let shell = user.shell();
|
||||
|
||||
let mut cmd = Command::new(shell);
|
||||
let cmd_arg0 = subsystem.as_deref().unwrap_or(shell);
|
||||
|
||||
// TODO: the SSH RFC mentions subsystems going through shell... should we?
|
||||
let mut cmd = Command::new(cmd_arg0);
|
||||
|
||||
if subsystem.is_none() {
|
||||
if let Some(shell_command) = req.command {
|
||||
cmd.arg("-c");
|
||||
cmd.arg(shell_command);
|
||||
}
|
||||
};
|
||||
|
||||
cmd.env_clear();
|
||||
|
||||
let has_pty = req.pty_term.is_some();
|
||||
|
|
@ -404,7 +425,7 @@ impl Server {
|
|||
cmd.env(k, v);
|
||||
}
|
||||
|
||||
debug!(cmd = %shell.display(), uid = %user.uid(), gid = %user.primary_group_id(), "Executing process");
|
||||
debug!(cmd = %cmd_arg0.display(), uid = %user.uid(), gid = %user.primary_group_id(), "Executing process");
|
||||
|
||||
let mut shell = cmd.spawn()?;
|
||||
|
||||
|
|
@ -533,12 +554,14 @@ impl Client {
|
|||
pub async fn shell(
|
||||
&self,
|
||||
command: Option<String>,
|
||||
subsystem: Option<String>,
|
||||
pty_term: Option<String>,
|
||||
env: Vec<(String, String)>,
|
||||
) -> Result<Vec<OwnedFd>> {
|
||||
self.send_request(&Request::Shell(ShellRequest {
|
||||
pty_term,
|
||||
command,
|
||||
subsystem,
|
||||
env,
|
||||
}))
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -105,6 +105,11 @@ pub enum ChannelRequest {
|
|||
|
||||
command: Vec<u8>,
|
||||
},
|
||||
Subsystem {
|
||||
want_reply: bool,
|
||||
|
||||
name: String,
|
||||
},
|
||||
Env {
|
||||
want_reply: bool,
|
||||
|
||||
|
|
@ -471,6 +476,19 @@ impl ChannelsState {
|
|||
command: command.to_owned(),
|
||||
}
|
||||
}
|
||||
"subsystem" => {
|
||||
if !self.is_server {
|
||||
return Err(peer_error!("server tried to set environment var"));
|
||||
}
|
||||
|
||||
let name = p.utf8_string()?;
|
||||
|
||||
info!(channel = %our_channel, %name, "Starting subsystem");
|
||||
ChannelRequest::Subsystem {
|
||||
want_reply,
|
||||
name: name.to_owned(),
|
||||
}
|
||||
}
|
||||
"env" => {
|
||||
if !self.is_server {
|
||||
return Err(peer_error!("server tried to set environment var"));
|
||||
|
|
@ -632,6 +650,7 @@ impl ChannelsState {
|
|||
Packet::new_msg_channel_request_shell(peer, b"shell", want_reply)
|
||||
}
|
||||
ChannelRequest::Exec { .. } => todo!("exec"),
|
||||
ChannelRequest::Subsystem { .. } => todo!("subsystem"),
|
||||
ChannelRequest::Env { .. } => todo!("env"),
|
||||
ChannelRequest::ExitStatus { status } => {
|
||||
Packet::new_msg_channel_request_exit_status(
|
||||
|
|
@ -787,6 +806,7 @@ impl ChannelOperation {
|
|||
ChannelRequest::PtyReq { .. } => "pty-req",
|
||||
ChannelRequest::Shell { .. } => "shell",
|
||||
ChannelRequest::Exec { .. } => "exec",
|
||||
ChannelRequest::Subsystem { .. } => "subsystem",
|
||||
ChannelRequest::Env { .. } => "env",
|
||||
ChannelRequest::ExitStatus { .. } => "exit-status",
|
||||
},
|
||||
|
|
|
|||
10
lib/cluelessh-sftp-proto/Cargo.toml
Normal file
10
lib/cluelessh-sftp-proto/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "cluelessh-sftp-proto"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cluelessh-transport = { path = "../cluelessh-transport" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
lib/cluelessh-sftp-proto/src/lib.rs
Normal file
1
lib/cluelessh-sftp-proto/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
9
lib/cluelessh-sftp/Cargo.toml
Normal file
9
lib/cluelessh-sftp/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "cluelessh-sftp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
14
lib/cluelessh-sftp/src/lib.rs
Normal file
14
lib/cluelessh-sftp/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue