#![cfg(all(unix, feature = "client", feature = "server", feature = "multichannel"))]
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use puressh::auth::{AuthAttempt, AuthDecision, Authenticator};
use puressh::client::{Client, Config as ClientConfig, HostKeyPolicy};
use puressh::hostkey::{Ed25519HostKey, HostKey};
use puressh::mux::{MasterConfig, Persist, ProbeOutcome, SessionRequest};
use puressh::server::{
AuthenticatorFactory, CommandHandler, Config as ServerConfig, ExecResult, Server, SessionEnv,
};
use puressh::shared::SharedClient;
struct CountingAuth {
user: String,
blob: Vec<u8>,
accepts: Arc<AtomicUsize>,
}
impl Authenticator for CountingAuth {
fn evaluate(&mut self, attempt: AuthAttempt) -> AuthDecision {
match attempt {
AuthAttempt::PublicKey {
user,
public_blob,
probe_only,
verified,
..
} => {
if user != self.user || public_blob != self.blob {
return AuthDecision::Reject;
}
if probe_only {
return AuthDecision::Accept;
}
if verified {
self.accepts.fetch_add(1, Ordering::SeqCst);
AuthDecision::Accept
} else {
AuthDecision::Reject
}
}
_ => AuthDecision::Reject,
}
}
}
struct BannerHandler {
out: Vec<u8>,
}
impl CommandHandler for BannerHandler {
fn handle(&self, _user: &str, _env: &SessionEnv, _command: &str) -> ExecResult {
ExecResult {
stdout: self.out.clone(),
stderr: Vec::new(),
exit_status: 0,
}
}
}
fn fresh_seed() -> [u8; 32] {
use purecrypto::rng::{OsRng, RngCore};
let mut s = [0u8; 32];
OsRng.fill_bytes(&mut s);
s
}
fn client_cfg() -> ClientConfig {
ClientConfig {
host_key_policy: HostKeyPolicy::AcceptAny,
timeout: Some(Duration::from_secs(15)),
algorithms: Default::default(),
}
}
struct TestServer {
addr: std::net::SocketAddr,
client_seed: [u8; 32],
user: String,
accepts: Arc<AtomicUsize>,
handle: thread::JoinHandle<()>,
}
fn spawn_server(banner: &[u8]) -> TestServer {
let host_seed = fresh_seed();
let client_seed = fresh_seed();
let host_key: Box<dyn HostKey + Send + Sync> = Box::new(Ed25519HostKey::from_seed(host_seed));
let allowed_blob = Ed25519HostKey::from_seed(client_seed).public_blob();
let user = "mux-user".to_string();
let accepts = Arc::new(AtomicUsize::new(0));
let auth_user = user.clone();
let auth_blob = allowed_blob.clone();
let auth_count = accepts.clone();
let factory: Arc<dyn AuthenticatorFactory> = Arc::new(move || -> Box<dyn Authenticator> {
Box::new(CountingAuth {
user: auth_user.clone(),
blob: auth_blob.clone(),
accepts: auth_count.clone(),
})
});
let cfg = ServerConfig::new(
vec![host_key],
factory,
vec!["publickey"],
Arc::new(BannerHandler {
out: banner.to_vec(),
}),
);
let mut server = Server::bind("127.0.0.1:0", cfg).expect("bind server");
let addr = server.local_addr().expect("server addr");
let handle = thread::spawn(move || {
let _ = server.accept_one();
});
TestServer {
addr,
client_seed,
user,
accepts,
handle,
}
}
fn connect_and_auth(srv: &TestServer) -> SharedClient {
let mut client =
Client::connect_to_host("127.0.0.1", srv.addr.port(), client_cfg()).expect("connect");
let hk: Box<dyn HostKey + Send> = Box::new(Ed25519HostKey::from_seed(srv.client_seed));
client
.authenticate_publickey(&srv.user, hk)
.expect("auth master");
client.into()
}
fn unique_socket_path(tag: &str) -> PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let pid = std::process::id();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
std::env::temp_dir().join(format!("puressh-mux-{tag}-{pid}-{nanos}.sock"))
}
#[test]
fn mux_client_reuses_connection_without_second_auth() {
let srv = spawn_server(b"banner-via-mux\n");
let shared = connect_and_auth(&srv);
assert_eq!(
srv.accepts.load(Ordering::SeqCst),
1,
"exactly one auth after the master connects"
);
let sock = unique_socket_path("reuse");
let release = Arc::new(AtomicBool::new(false));
let fg_release = release.clone();
let cfg = MasterConfig {
control_path: sock.clone(),
persist: Persist::No,
};
let master = thread::spawn(move || {
puressh::mux::run_master(cfg, shared, move |_s| {
while !fg_release.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(10));
}
0
})
});
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if puressh::mux::probe_master(&sock) == ProbeOutcome::Live {
break;
}
assert!(Instant::now() < deadline, "master never came up");
thread::sleep(Duration::from_millis(20));
}
let req = SessionRequest {
want_pty: false,
term: String::new(),
cols: 0,
rows: 0,
env: vec![],
command: Some("echo hi".into()),
};
let status = puressh::mux::run_client(&sock, &req, None).expect("mux client run");
assert_eq!(status, 0, "remote exec exit status");
assert_eq!(
srv.accepts.load(Ordering::SeqCst),
1,
"mux client must not trigger a second auth"
);
release.store(true, Ordering::SeqCst);
let _ = master.join().expect("master thread");
assert!(!sock.exists(), "Persist::No unlinks the control socket");
drop(srv.accepts);
let _ = srv.handle.join();
}
#[test]
fn control_persist_seconds_master_exits_after_idle() {
let srv = spawn_server(b"x\n");
let shared = connect_and_auth(&srv);
let sock = unique_socket_path("persist");
let cfg = MasterConfig {
control_path: sock.clone(),
persist: Persist::Seconds(1),
};
let _ = puressh::mux::run_master(cfg, shared, |_s| 0).expect("run_master");
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if puressh::mux::probe_master(&sock) == ProbeOutcome::Live {
break;
}
assert!(Instant::now() < deadline, "master never came up");
thread::sleep(Duration::from_millis(20));
}
let gone_by = Instant::now() + Duration::from_secs(6);
while sock.exists() {
assert!(
Instant::now() < gone_by,
"ControlPersist=1 master did not exit after idle"
);
thread::sleep(Duration::from_millis(50));
}
drop(srv.accepts);
let _ = srv.handle.join();
}
#[test]
fn run_master_daemon_serves_then_exits_after_idle() {
let srv = spawn_server(b"daemon-banner\n");
let shared = connect_and_auth(&srv);
assert_eq!(srv.accepts.load(Ordering::SeqCst), 1, "one auth at master");
let sock = unique_socket_path("daemon");
let cfg = MasterConfig {
control_path: sock.clone(),
persist: Persist::Seconds(1),
};
let daemon = thread::spawn(move || {
let _ = puressh::mux::run_master_daemon(cfg, shared);
});
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if puressh::mux::probe_master(&sock) == ProbeOutcome::Live {
break;
}
assert!(Instant::now() < deadline, "daemon master never came up");
thread::sleep(Duration::from_millis(20));
}
let req = SessionRequest {
want_pty: false,
term: String::new(),
cols: 0,
rows: 0,
env: vec![],
command: Some("echo hi".into()),
};
let status = puressh::mux::run_client(&sock, &req, None).expect("mux client run");
assert_eq!(status, 0, "remote exec exit status over daemon");
assert_eq!(
srv.accepts.load(Ordering::SeqCst),
1,
"daemon mux client must not trigger a second auth"
);
let gone_by = Instant::now() + Duration::from_secs(6);
while sock.exists() {
assert!(
Instant::now() < gone_by,
"daemon master did not exit after idle linger"
);
thread::sleep(Duration::from_millis(50));
}
let _ = daemon.join();
drop(srv.accepts);
let _ = srv.handle.join();
}
#[test]
fn control_command_check_and_exit() {
use puressh::mux::ControlCommand;
let srv = spawn_server(b"octl\n");
let shared = connect_and_auth(&srv);
let sock = unique_socket_path("octl");
assert!(
!puressh::mux::send_control_command(&sock, ControlCommand::Check).expect("check absent"),
"check on an absent control path must report no master"
);
let cfg = MasterConfig {
control_path: sock.clone(),
persist: Persist::Yes,
};
let release = Arc::new(AtomicBool::new(false));
let fg_release = release.clone();
let master = thread::spawn(move || {
puressh::mux::run_master(cfg, shared, move |_s| {
while !fg_release.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(10));
}
0
})
});
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if puressh::mux::probe_master(&sock) == ProbeOutcome::Live {
break;
}
assert!(Instant::now() < deadline, "master never came up");
thread::sleep(Duration::from_millis(20));
}
assert!(
puressh::mux::send_control_command(&sock, ControlCommand::Check).expect("check live"),
"check on a live master must report it alive"
);
assert!(
puressh::mux::send_control_command(&sock, ControlCommand::Exit).expect("exit"),
"exit request must be delivered"
);
let gone_by = Instant::now() + Duration::from_secs(5);
while sock.exists() {
assert!(
Instant::now() < gone_by,
"-O exit did not tear the master down / unlink the socket"
);
thread::sleep(Duration::from_millis(50));
}
assert!(
!puressh::mux::send_control_command(&sock, ControlCommand::Check).expect("check post-exit"),
"check after exit must report no master"
);
release.store(true, Ordering::SeqCst);
let _ = master.join();
drop(srv.accepts);
let _ = srv.handle.join();
}
fn spawn_server_direct_tcpip(banner: &[u8]) -> TestServer {
use puressh::forwarding::direct::DefaultDirectTcpipHandler;
let host_seed = fresh_seed();
let client_seed = fresh_seed();
let host_key: Box<dyn HostKey + Send + Sync> = Box::new(Ed25519HostKey::from_seed(host_seed));
let allowed_blob = Ed25519HostKey::from_seed(client_seed).public_blob();
let user = "mux-user".to_string();
let accepts = Arc::new(AtomicUsize::new(0));
let auth_user = user.clone();
let auth_blob = allowed_blob.clone();
let auth_count = accepts.clone();
let factory: Arc<dyn AuthenticatorFactory> = Arc::new(move || -> Box<dyn Authenticator> {
Box::new(CountingAuth {
user: auth_user.clone(),
blob: auth_blob.clone(),
accepts: auth_count.clone(),
})
});
let cfg = ServerConfig::new(
vec![host_key],
factory,
vec!["publickey"],
Arc::new(BannerHandler {
out: banner.to_vec(),
}),
)
.with_direct_tcpip(Arc::new(DefaultDirectTcpipHandler::permit_all()));
let mut server = Server::bind("127.0.0.1:0", cfg).expect("bind server");
let addr = server.local_addr().expect("server addr");
let handle = thread::spawn(move || {
let _ = server.accept_one();
});
TestServer {
addr,
client_seed,
user,
accepts,
handle,
}
}
fn spawn_echo_server() -> (u16, thread::JoinHandle<()>) {
use std::io::{Read, Write};
use std::net::TcpListener;
let listener = TcpListener::bind("127.0.0.1:0").expect("bind echo");
let port = listener.local_addr().unwrap().port();
let handle = thread::spawn(move || {
if let Ok((mut sock, _)) = listener.accept() {
let mut buf = [0u8; 1024];
loop {
match sock.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if sock.write_all(&buf[..n]).is_err() {
break;
}
}
}
}
}
});
(port, handle)
}
#[test]
fn local_forward_over_mux_client() {
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
let srv = spawn_server_direct_tcpip(b"unused\n");
let shared = connect_and_auth(&srv);
assert_eq!(srv.accepts.load(Ordering::SeqCst), 1, "one auth at master");
let (echo_port, echo_handle) = spawn_echo_server();
let sock = unique_socket_path("lforward");
let release = Arc::new(AtomicBool::new(false));
let fg_release = release.clone();
let cfg = MasterConfig {
control_path: sock.clone(),
persist: Persist::No,
};
let master = thread::spawn(move || {
puressh::mux::run_master(cfg, shared, move |_s| {
while !fg_release.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(10));
}
0
})
});
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if puressh::mux::probe_master(&sock) == ProbeOutcome::Live {
break;
}
assert!(Instant::now() < deadline, "master never came up");
thread::sleep(Duration::from_millis(20));
}
let acceptor = TcpListener::bind("127.0.0.1:0").expect("bind local -L");
let local_port = acceptor.local_addr().unwrap().port();
let client_side = TcpStream::connect(("127.0.0.1", local_port)).expect("connect local -L");
let (server_side, _) = acceptor.accept().expect("accept local -L");
let mux_path = sock.clone();
let worker = thread::spawn(move || {
let fwd = puressh::mux::open_forward(&mux_path, "127.0.0.1", echo_port, "127.0.0.1", 0)
.expect("open_forward over mux");
let _ = puressh::mux::splice_forward(fwd, server_side);
});
let mut client_side = client_side;
client_side.write_all(b"mux -L works").expect("write");
let mut buf = [0u8; 12];
client_side.read_exact(&mut buf).expect("read echo");
assert_eq!(&buf, b"mux -L works");
assert_eq!(
srv.accepts.load(Ordering::SeqCst),
1,
"mux forward must not trigger a second auth"
);
drop(client_side);
let _ = worker.join();
release.store(true, Ordering::SeqCst);
let _ = master.join().expect("master thread");
let _ = echo_handle.join();
drop(srv.accepts);
let _ = srv.handle.join();
}