use clap::Parser;
use qssh::{QsshConfig, PqAlgorithm, QsshClient, ReconnectConfig, security_tiers::SecurityTier};
use qssh::config::ConfigParser;
use qssh::port_forward::PortForwardManager;
use qssh::multiplex::{ControlMaster, ControlClient};
use qssh::proxy::{ProxyConnection, ProxyConfig, ProxyType};
use log::info;
use std::process;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Parser, Debug)]
#[clap(name = "qssh")]
#[clap(about = "Quantum-Secure Shell - Connect to remote systems with post-quantum security")]
#[clap(version = env!("CARGO_PKG_VERSION"))]
struct Args {
destination: String,
#[clap(short = 'p', long, default_value = "22222")]
port: u16,
#[clap(long)]
qkd: bool,
#[clap(long)]
qkd_endpoint: Option<String>,
#[clap(long)]
qkd_cert: Option<String>,
#[clap(long)]
qkd_key: Option<String>,
#[clap(long)]
qkd_ca: Option<String>,
#[clap(short = 'L', long)]
local: Vec<String>,
#[clap(short = 'R', long)]
remote: Vec<String>,
#[clap(short = 'D', long)]
dynamic: Vec<String>,
#[clap(long, default_value = "sphincs")]
pq_algo: String,
#[clap(short = 'c', long)]
command: Option<String>,
#[clap(short = 'P', long)]
use_password: bool,
#[clap(short = 'X', long)]
x11: bool,
#[clap(short = 'Y', long)]
trusted_x11: bool,
#[clap(short = 'A', long = "agent-forward")]
agent_forward: bool,
#[clap(short, long)]
verbose: bool,
#[clap(long)]
quantum_native: bool,
#[clap(long)]
classical: bool,
#[clap(long)]
persistent: bool,
#[clap(long, default_value = "10")]
max_retries: u32,
#[clap(short = 'S', long = "ctl-path")]
ctl_path: Option<String>,
#[clap(short = 'J', long = "proxy-jump")]
jump: Option<String>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
if args.verbose {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug"))
.target(env_logger::Target::Stderr)
.init();
} else {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn"))
.target(env_logger::Target::Stderr)
.init();
}
let config_parser = ConfigParser::load_default().unwrap_or_else(|e| {
if args.verbose {
eprintln!("Warning: Failed to load config file: {}", e);
}
ConfigParser::parse("").unwrap()
});
let (username, hostname) = if args.destination.contains('@') {
let parts: Vec<&str> = args.destination.split('@').collect();
if parts.len() != 2 {
eprintln!("Error: Invalid destination format. Use user@host or host");
process::exit(1);
}
(Some(parts[0].to_string()), parts[1].to_string())
} else {
(None, args.destination.clone())
};
let host_config = config_parser.get_host_config(&hostname);
let username = username.or(host_config.user).unwrap_or_else(|| {
eprintln!("Error: Username not specified. Use user@host or configure in ~/.qssh/config");
process::exit(1);
});
let port = if args.port != 22222 {
args.port
} else {
host_config.port.unwrap_or(22222)
};
let host = if let Some(h) = host_config.hostname {
format!("{}:{}", h, port)
} else {
format!("{}:{}", hostname, port)
};
let mut local_forwards = Vec::new();
for spec in &args.local {
match PortForwardManager::parse_forward_spec(spec, "local") {
Ok(fwd) => local_forwards.push(fwd),
Err(e) => {
eprintln!("Error: Invalid local forward spec '{}': {}", spec, e);
process::exit(1);
}
}
}
let pq_algorithm = match args.pq_algo.as_str() {
"sphincs" => PqAlgorithm::SphincsPlus,
"falcon" | "falcon512" => PqAlgorithm::Falcon512,
"falcon1024" => PqAlgorithm::Falcon1024,
"kyber" | "kyber512" | "kyber768" | "kyber1024" => {
eprintln!("Warning: Kyber algorithms are vulnerable to KyberSlash attacks.");
eprintln!("Using Falcon-512 instead for quantum security.");
PqAlgorithm::Falcon512
},
_ => {
eprintln!("Error: Unknown PQ algorithm: {}", args.pq_algo);
eprintln!("Available algorithms: sphincs, falcon512, falcon1024");
process::exit(1);
}
};
let password = if args.use_password {
eprint!("Password: ");
rpassword::read_password().ok()
} else {
None
};
let quantum_native = !args.classical;
let config = QsshConfig {
server: host.clone(),
username: username.clone(),
password,
port_forwards: Vec::new(), use_qkd: args.qkd,
qkd_endpoint: args.qkd_endpoint.clone(),
qkd_cert_path: args.qkd_cert.clone(),
qkd_key_path: args.qkd_key.clone(),
qkd_ca_path: args.qkd_ca.clone(),
pq_algorithm,
kex_algorithm: host_config.kex_algorithm.unwrap_or_default(),
key_rotation_interval: 3600, security_tier: SecurityTier::default(),
quantum_native,
};
info!("Connecting to {} as {}...", host, username);
info!("Using post-quantum algorithm: {:?}", pq_algorithm);
if quantum_native {
info!("Transport: Quantum-native (768-byte indistinguishable frames)");
} else {
info!("Transport: Classical (variable-size frames)");
}
if args.qkd {
info!("QKD enabled");
if let Some(endpoint) = args.qkd_endpoint {
info!("QKD endpoint: {}", endpoint);
}
}
let mut remote_forwards = Vec::new();
for spec in &args.remote {
match PortForwardManager::parse_forward_spec(spec, "remote") {
Ok(fwd) => remote_forwards.push(fwd),
Err(e) => {
eprintln!("Error: Invalid remote forward spec '{}': {}", spec, e);
process::exit(1);
}
}
}
let mut dynamic_forwards = Vec::new();
for spec in &args.dynamic {
match PortForwardManager::parse_forward_spec(spec, "dynamic") {
Ok(fwd) => dynamic_forwards.push(fwd),
Err(e) => {
eprintln!("Error: Invalid dynamic forward spec '{}': {}", spec, e);
process::exit(1);
}
}
}
let has_remote_forwards = !remote_forwards.is_empty();
let has_forwards = !local_forwards.is_empty() || !remote_forwards.is_empty() || !dynamic_forwards.is_empty();
if let Some(ref ctl_path_str) = args.ctl_path {
let ctl_path = expand_tilde(ctl_path_str);
if ControlMaster::check_master(&ctl_path).await {
info!("Reusing existing connection via control master at {:?}", ctl_path);
let mut mux_client = ControlClient::new(ctl_path);
match mux_client.connect().await {
Ok(()) => {
match mux_client.new_session(args.command.clone()).await {
Ok(session_id) => {
info!("Multiplexed session {} created", session_id);
eprintln!("Session {} active on existing connection. Press Ctrl+C to close.", session_id);
tokio::signal::ctrl_c().await.ok();
let _ = mux_client.close_session(session_id).await;
}
Err(e) => {
eprintln!("Failed to create multiplexed session: {}", e);
process::exit(1);
}
}
}
Err(e) => {
eprintln!("Failed to connect to control master: {}", e);
process::exit(1);
}
}
return;
}
}
let reconnect_config = if args.persistent {
Some(ReconnectConfig {
max_attempts: args.max_retries,
..ReconnectConfig::default()
})
} else {
None
};
let mut session_count = 0u32;
loop {
session_count += 1;
let mut client = QsshClient::new(config.clone());
let connect_result = if let Some(ref jump_spec) = args.jump {
info!("Using ProxyJump through: {}", jump_spec);
let proxy_config = ProxyConfig {
proxy_type: ProxyType::Jump,
target: jump_spec.clone(),
options: Vec::new(),
};
let proxy = ProxyConnection::new(proxy_config, config.clone());
let parts: Vec<&str> = config.server.split(':').collect();
let proxy_host = parts[0];
let proxy_port: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(22222);
match proxy.connect(proxy_host, proxy_port).await {
Ok(stream) => client.connect_via_stream(stream).await,
Err(e) => Err(e),
}
} else if let Some(ref rc) = reconnect_config {
client.connect_with_retry(rc).await
} else {
client.connect().await
};
match connect_result {
Ok(()) => {
if session_count > 1 {
eprintln!("Reconnected (session #{})", session_count);
}
info!("Connected successfully!");
if let Some(ref ctl_path_str) = args.ctl_path {
if let Some(transport) = client.transport() {
let ctl_path = expand_tilde(ctl_path_str);
let master = ControlMaster::new(ctl_path.clone(), Arc::new(transport.clone()));
info!("Starting control master at {:?}", ctl_path);
tokio::spawn(async move {
if let Err(e) = master.start().await {
log::error!("Control master error: {}", e);
}
});
}
}
if has_forwards {
if let Some(transport) = client.transport() {
let transport = Arc::new(transport.clone());
let mut pfm = PortForwardManager::new(transport);
for fwd in &local_forwards {
pfm.add_forward(fwd.clone());
}
for fwd in &remote_forwards {
pfm.add_forward(fwd.clone());
}
for fwd in &dynamic_forwards {
pfm.add_forward(fwd.clone());
}
match pfm.start_all().await {
Ok(()) => {
info!("Port forwards established");
client.set_remote_forward_state(
pfm.remote_registry(),
pfm.channel_router(),
);
}
Err(e) => {
eprintln!("Error setting up port forwards: {}", e);
if reconnect_config.is_none() { process::exit(1); }
}
}
}
}
if args.x11 || args.trusted_x11 {
match client.enable_x11(args.trusted_x11).await {
Ok(()) => info!("X11 forwarding enabled"),
Err(e) => eprintln!("Warning: Failed to enable X11 forwarding: {}", e),
}
}
if args.agent_forward {
match client.enable_agent_forwarding().await {
Ok(()) => info!("Agent forwarding enabled"),
Err(e) => eprintln!("Warning: Failed to enable agent forwarding: {}", e),
}
}
if let Some(ref command) = args.command {
match client.exec_with_status(command).await {
Ok((output, exit_code)) => {
print!("{}", output);
if exit_code != 0 && reconnect_config.is_none() && !has_remote_forwards && !has_forwards {
process::exit(exit_code as i32);
}
}
Err(e) => {
eprintln!("Command execution failed: {}", e);
if reconnect_config.is_none() { process::exit(1); }
}
}
if has_remote_forwards || has_forwards {
info!("Exec completed, keeping connection alive for port forwards");
info!("Press Ctrl+C to disconnect");
if let Err(e) = client.run_forward_loop().await {
log::debug!("Forward loop ended: {}", e);
}
}
} else {
match client.shell().await {
Ok(_) => info!("Shell session terminated normally"),
Err(e) => {
eprintln!("Shell error: {}", e);
eprintln!("Error details: {:?}", e);
}
}
}
let _ = client.disconnect().await;
if let Some(ref ctl_path_str) = args.ctl_path {
let ctl_path = expand_tilde(ctl_path_str);
let _ = std::fs::remove_file(&ctl_path);
}
}
Err(e) => {
eprintln!("Connection failed: {}", e);
if reconnect_config.is_none() { process::exit(1); }
process::exit(1);
}
}
if reconnect_config.is_none() {
break;
}
eprintln!("Session ended. Reconnecting in 1s...");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
fn expand_tilde(path: &str) -> PathBuf {
if path.starts_with("~/") {
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(format!("{}{}", home, &path[1..]));
}
}
PathBuf::from(path)
}