Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,265 changes: 619 additions & 646 deletions Cargo.lock

Large diffs are not rendered by default.

61 changes: 31 additions & 30 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,75 +17,76 @@ categories = ["command-line-utilities"]
edition = "2021"

[dependencies]
tokio = { version = "1.48.0", features = ["full"] }
bytes = "1"
tokio = { version = "1.50.0", features = ["full"] }
# Use our internal russh fork with session loop fixes
# - Development: uses local path (crates/bssh-russh)
# - Publishing: uses crates.io version (path ignored)
russh = { package = "bssh-russh", version = "0.56", path = "crates/bssh-russh" }
russh = { package = "bssh-russh", version = "0.59", path = "crates/bssh-russh" }
russh-sftp = "2.1.1"
clap = { version = "4.5.53", features = ["derive", "env"] }
anyhow = "1.0.100"
thiserror = "2.0.17"
clap = { version = "4.6.0", features = ["derive", "env"] }
anyhow = "1.0.102"
thiserror = "2.0.18"
tracing = "0.1.43"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_yaml = "0.9"
futures = "0.3.31"
futures = "0.3.32"
async-trait = "0.1.89"
indicatif = "0.18.3"
indicatif = "0.18.4"
rpassword = "7.4.0"
directories = "6.0.0"
dirs = "6.0"
chrono = { version = "0.4.42", features = ["serde"] }
chrono = { version = "0.4.44", features = ["serde"] }
glob = "0.3.3"
whoami = "2.0.1"
owo-colors = "4.2.3"
whoami = "2.1.1"
owo-colors = "4.3.0"
unicode-width = "0.2.2"
terminal_size = "0.4.3"
once_cell = "1.21.3"
terminal_size = "0.4.4"
once_cell = "1.21.4"
zeroize = { version = "1.8.2", features = ["derive"] }
secrecy = { version = "0.10.3", features = ["serde"] }
rustyline = "17.0.2"
rustyline = "18.0.0"
crossterm = "0.29"
ratatui = "0.30"
regex = "1.12.2"
regex = "1.12.3"
lazy_static = "1.5"
ctrlc = "3.5.1"
signal-hook = "0.4.1"
nix = { version = "0.30", features = ["fs", "poll", "process", "signal", "term"] }
ctrlc = "3.5.2"
signal-hook = "0.4.3"
nix = { version = "0.31", features = ["fs", "poll", "process", "signal", "term"] }
atty = "0.2.14"
arrayvec = "0.7.6"
smallvec = "1.15.1"
lru = "0.16.2"
uuid = { version = "1.19.0", features = ["v4"] }
uuid = { version = "1.23.0", features = ["v4"] }
fastrand = "2.3.0"
tokio-util = "0.7.17"
shell-words = "1.1.1"
libc = "0.2"
ipnetwork = "0.20"
bcrypt = "0.16"
ipnetwork = "0.21"
bcrypt = "0.19"
argon2 = "0.5"
rand = "0.8"
ssh-key = { version = "0.6", features = ["std"] }
async-compression = { version = "0.4", features = ["tokio", "gzip"] }
serde_json = "1.0"
opentelemetry = "0.21"
opentelemetry_sdk = { version = "0.21", features = ["rt-tokio", "logs"] }
opentelemetry-otlp = { version = "0.14", features = ["grpc-tonic", "logs"] }
opentelemetry = "0.31"
opentelemetry_sdk = { version = "0.31", features = ["rt-tokio", "logs"] }
opentelemetry-otlp = { version = "0.31", features = ["grpc-tonic", "logs"] }
url = "2.5"
tokio-rustls = "0.26"
rustls-native-certs = "0.8"

[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "3.5.1"
security-framework = "3.7.0"

[dev-dependencies]
tempfile = "3.23.0"
mockito = "1.7.1"
once_cell = "1.21.3"
tempfile = "3.27.0"
mockito = "1.7.2"
once_cell = "1.21.4"
tokio-test = "0.4"
serial_test = "3.2"
insta = "1.44"
serial_test = "3.4"
insta = "1.47"
criterion = { version = "0.8", features = ["html_reports"] }
mockall = "0.14"

Expand Down
12 changes: 6 additions & 6 deletions benches/large_output_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ use bssh::executor::{MultiNodeStreamManager, NodeStream};
use bssh::node::Node;
use bssh::ssh::tokio_client::CommandOutput;
use bssh::ui::tui::app::TuiApp;
use bytes::Bytes;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use ratatui::backend::TestBackend;
use ratatui::Terminal;
use russh::CryptoVec;
use std::hint::black_box;
use tokio::runtime::Runtime;
use tokio::sync::mpsc;
Expand Down Expand Up @@ -65,7 +65,7 @@ fn bench_large_output_single_stream(c: &mut Criterion) {

// Send data in 32KB chunks (typical SSH packet size)
let chunk_size = 32 * 1024;
let chunk = CryptoVec::from(vec![b'x'; chunk_size.min(size)]);
let chunk = Bytes::from(vec![b'x'; chunk_size.min(size)]);
let num_chunks = size.div_ceil(chunk_size);

for _ in 0..num_chunks {
Expand Down Expand Up @@ -110,7 +110,7 @@ fn bench_rolling_buffer_overflow(c: &mut Criterion) {

// Send data in chunks to exceed buffer limit
let chunk_size = 64 * 1024; // 64KB chunks
let chunk = CryptoVec::from(vec![b'x'; chunk_size]);
let chunk = Bytes::from(vec![b'x'; chunk_size]);
let num_chunks = total_size / chunk_size;

for _ in 0..num_chunks {
Expand Down Expand Up @@ -162,7 +162,7 @@ fn bench_concurrent_multi_node(c: &mut Criterion) {

// Send data to all nodes
let data_per_node = 100 * 1024; // 100KB per node
let chunk = CryptoVec::from(vec![b'x'; 1024]);
let chunk = Bytes::from(vec![b'x'; 1024]);
let chunks_per_node = data_per_node / 1024;

for _ in 0..chunks_per_node {
Expand Down Expand Up @@ -214,7 +214,7 @@ fn bench_poll_all_throughput(c: &mut Criterion) {
senders.push(tx);
}

let chunk = CryptoVec::from(vec![b'x'; chunk_size]);
let chunk = Bytes::from(vec![b'x'; chunk_size]);

// Send one chunk to each node and poll
for tx in &senders {
Expand Down Expand Up @@ -304,7 +304,7 @@ fn bench_tui_render_detail(c: &mut Criterion) {
}

rt.block_on(async {
tx.send(CommandOutput::StdOut(CryptoVec::from(
tx.send(CommandOutput::StdOut(Bytes::from(
output.as_bytes().to_vec(),
)))
.await
Expand Down
20 changes: 10 additions & 10 deletions crates/bssh-russh/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bssh-russh"
version = "0.56.0"
version = "0.59.0"
authors = ["Jeongkyu Shin <inureyes@gmail.com>"]
description = "Temporary fork of russh with high-frequency PTY output fix (Handle::data from spawned tasks)"
documentation = "https://docs.rs/bssh-russh"
Expand All @@ -21,11 +21,12 @@ des = ["dep:des"]
dsa = ["ssh-key/dsa"]
ring = ["dep:ring"]
rsa = ["dep:rsa", "dep:pkcs1", "ssh-key/rsa", "ssh-key/rsa-sha1"]
serde = ["ssh-key/serde"]

[dependencies]
aes = "0.8"
async-trait = { version = "0.1.50", optional = true }
aws-lc-rs = { version = "1.13.1", optional = true }
aws-lc-rs = { version = "1.16.2", optional = true }
bitflags = "2.0"
block-padding = { version = "0.3", features = ["std"] }
byteorder = "1.4"
Expand All @@ -46,12 +47,12 @@ flate2 = { version = "1.0.15", optional = true }
futures = "0.3"
generic-array = { version = "1.3.3", features = ["compat-0_14"] }
getrandom = { version = "0.2.15", features = ["js"] }
hex-literal = "0.4"
hex-literal = "1"
hmac = "0.12"
inout = { version = "0.1", features = ["std"] }
libcrux-ml-kem = "0.0.4"
log = "0.4"
md5 = "0.7"
ml-kem = "0.2.3"
num-bigint = { version = "0.4.2", features = ["rand"] }
p256 = { version = "0.13", features = ["ecdh"] }
p384 = { version = "0.13", features = ["ecdh"] }
Expand All @@ -60,8 +61,8 @@ pbkdf2 = "0.12"
pkcs1 = { version = "0.8.0-rc.4", optional = true }
pkcs5 = "0.7"
pkcs8 = { version = "0.10", features = ["pkcs5", "encryption", "std"] }
rand_core = { version = "0.6.4", features = ["getrandom", "std"] }
rand = "0.8"
rand_core = { version = "=0.10.0-rc-3" }
rand = { version = "0.9", features = ["thread_rng"] }
ring = { version = "0.17.14", optional = true }
rsa = { version = "0.10.0-rc.10", optional = true }
sec1 = { version = "0.7", features = ["pkcs8", "der"] }
Expand All @@ -71,15 +72,14 @@ signature = "2.2"
spki = "0.7"
ssh-encoding = { version = "0.2", features = ["bytes"] }
subtle = "2.4"
thiserror = "1.0.30"
tokio = { version = "1.48.0", features = ["io-util", "sync", "time", "rt-multi-thread", "net"] }
thiserror = "2.0.18"
tokio = { version = "1.50.0", features = ["io-util", "sync", "time", "rt-multi-thread", "net"] }
typenum = "1.17"
yasna = { version = "0.5.0", features = ["bit-vec", "num-bigint"], optional = true }
zeroize = "1.7"
home = "0.5"

# Public russh crates (no modifications needed)
russh-cryptovec = { version = "0.52.0", features = ["ssh-encoding"] }
russh-cryptovec = { version = "0.59.0", features = ["ssh-encoding"] }
russh-util = "0.52.0"

# Use the forked ssh-key from russh
Expand Down
11 changes: 3 additions & 8 deletions crates/bssh-russh/patches/handle-data-fix.patch
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- a/src/server/session.rs 2026-01-23 18:47:48
+++ b/src/server/session.rs 2026-01-24 03:08:34
--- /tmp/russh-upstream-compare/russh/src/server/session.rs 2026-04-03 13:17:42
+++ /Users/inureyes/Development/backend.ai/bssh/crates/bssh-russh/src/server/session.rs 2026-04-03 13:20:54
@@ -7,7 +7,7 @@
use log::debug;
use negotiation::parse_kex_algo_list;
Expand All @@ -9,12 +9,7 @@
use tokio::sync::oneshot;

use super::*;
@@ -502,10 +502,141 @@
pin!(reading);
let mut is_reading = None;

+
#[allow(clippy::panic)] // false positive in macro
@@ -513,6 +513,136 @@
while !self.common.disconnected {
self.common.received_data = false;
let mut sent_keepalive = false;
Expand Down
28 changes: 17 additions & 11 deletions crates/bssh-russh/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ use ssh_key::{Certificate, HashAlg, PrivateKey};
use thiserror::Error;
use tokio::io::{AsyncRead, AsyncWrite};

use crate::CryptoVec;
use crate::helpers::NameList;
use crate::keys::PrivateKeyWithHashAlg;
use crate::keys::agent::AgentIdentity;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodKind {
Expand Down Expand Up @@ -157,12 +157,12 @@ impl AuthResult {
pub trait Signer: Sized {
type Error: From<crate::SendError>;

fn auth_publickey_sign(
fn auth_sign(
&mut self,
key: &ssh_key::PublicKey,
key: &AgentIdentity,
hash_alg: Option<HashAlg>,
to_sign: CryptoVec,
) -> impl Future<Output = Result<CryptoVec, Self::Error>> + Send;
to_sign: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, Self::Error>> + Send;
}

#[derive(Debug, Error)]
Expand All @@ -180,12 +180,12 @@ impl<R: AsyncRead + AsyncWrite + Unpin + Send + 'static> Signer
type Error = AgentAuthError;

#[allow(clippy::manual_async_fn)]
fn auth_publickey_sign(
fn auth_sign(
&mut self,
key: &ssh_key::PublicKey,
key: &AgentIdentity,
hash_alg: Option<HashAlg>,
to_sign: CryptoVec,
) -> impl Future<Output = Result<CryptoVec, Self::Error>> {
to_sign: Vec<u8>,
) -> impl Future<Output = Result<Vec<u8>, Self::Error>> {
async move {
self.sign_request(key, hash_alg, to_sign)
.await
Expand All @@ -212,6 +212,12 @@ pub enum Method {
key: ssh_key::PublicKey,
hash_alg: Option<HashAlg>,
},
/// Certificate-based authentication using an external signer (e.g., SSH agent).
/// The certificate is sent to the server, but signing is delegated to the signer.
FutureCertificate {
cert: Certificate,
hash_alg: Option<HashAlg>,
},
KeyboardInteractive {
submethods: String,
},
Expand All @@ -235,9 +241,9 @@ pub enum CurrentRequest {
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
PublicKey {
#[allow(dead_code)]
key: CryptoVec,
key: Vec<u8>,
#[allow(dead_code)]
algo: CryptoVec,
algo: Vec<u8>,
sent_pk_ok: bool,
},
KeyboardInteractive {
Expand Down
8 changes: 4 additions & 4 deletions crates/bssh-russh/src/channels/io/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ use tokio::sync::mpsc::error::SendError;
use tokio::sync::mpsc::{self, OwnedPermit};
use tokio::sync::{Mutex, Notify, OwnedMutexGuard};

use bytes::Bytes;

use super::ChannelMsg;
use crate::{ChannelId, CryptoVec};
use crate::ChannelId;

type BoxedThreadsafeFuture<T> = Pin<Box<dyn Sync + Send + std::future::Future<Output = T>>>;
type OwnedPermitFuture<S> =
Expand Down Expand Up @@ -112,10 +114,8 @@ where
) -> Poll<(ChannelMsg, NonZeroUsize)> {
let writable = ready!(self.poll_writable(cx, buf.len()));

let mut data = CryptoVec::new_zeroed(writable.into());
#[allow(clippy::indexing_slicing)] // Clamped to maximum `buf.len()` with `.poll_writable`
data.copy_from_slice(&buf[..writable.into()]);
data.resize(writable.into());
let data = Bytes::copy_from_slice(&buf[..writable.into()]);

let msg = match self.ext {
None => ChannelMsg::Data { data },
Expand Down
7 changes: 4 additions & 3 deletions crates/bssh-russh/src/channels/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use std::sync::Arc;

use bytes::Bytes;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::sync::mpsc::{Receiver, Sender};
use tokio::sync::{Mutex, Notify};

use crate::{ChannelId, ChannelOpenFailure, CryptoVec, Error, Pty, Sig};
use crate::{ChannelId, ChannelOpenFailure, Error, Pty, Sig};

pub mod io;

Expand All @@ -24,10 +25,10 @@ pub enum ChannelMsg {
window_size: u32,
},
Data {
data: CryptoVec,
data: Bytes,
},
ExtendedData {
data: CryptoVec,
data: Bytes,
ext: u32,
},
Eof,
Expand Down
9 changes: 5 additions & 4 deletions crates/bssh-russh/src/cipher/benchmark.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#![allow(clippy::unwrap_used)]
use criterion::*;
use rand::RngCore;
use rand::TryRngCore;
use std::hint;

pub fn bench(c: &mut Criterion) {
let mut rand_generator = black_box(rand::rngs::OsRng {});
let mut rand_generator = hint::black_box(rand::rngs::OsRng {});

let mut packet_length = black_box(vec![0u8; 4]);
let mut packet_length = hint::black_box(vec![0u8; 4]);

for cipher_name in [super::CHACHA20_POLY1305, super::AES_256_GCM] {
let cipher = super::CIPHERS.get(&cipher_name).unwrap();
Expand All @@ -26,7 +27,7 @@ pub fn bench(c: &mut Criterion) {
group.bench_function(format!("Block size: {size}"), |b| {
b.iter_with_setup(
|| {
let mut in_out = black_box(vec![0u8; size]);
let mut in_out = hint::black_box(vec![0u8; size]);
rand_generator.try_fill_bytes(&mut in_out).unwrap();
rand_generator.try_fill_bytes(&mut packet_length).unwrap();
in_out
Expand Down
Loading
Loading