use std::{
io::Write,
net::{IpAddr, Ipv6Addr, SocketAddr, ToSocketAddrs},
time::Duration,
};
use alkali::asymmetric::kx::x25519blake2b as X25519;
use base64::prelude::{Engine as _, BASE64_STANDARD_NO_PAD};
use clap::Parser;
use indoc::indoc;
use once_cell::sync::Lazy;
use regex::{self, Regex};
use rustyline_async::{Readline, ReadlineEvent};
use tokio::{net::TcpListener, signal};
use crate::{
protocol::{Command, Connection, ExitBreak, OutputType, SecureMsg, SetupSecret, TerminateCmd},
sigint, util,
};
#[derive(Parser, Debug)]
pub struct Cli {
#[arg(required = true, value_name = "HOST:PORT")]
callback: String,
#[arg(long)]
local_port: Option<u16>,
}
struct Params {
pub callback: String,
pub listen: SocketAddr,
}
const UNSPECIFIED_IP: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
fn parse_args(args: &Cli) -> Result<Params, String> {
let (_, callback_port) = util::parse_address(&args.callback)
.map_err(|err| format!("Invalid callback address: {err}"))?;
match &args.callback.to_socket_addrs() {
Ok(addrs) => {
if addrs.len() == 0 {
eprintln!("[WARNING] Callback host did not resolve to any IP addresses.");
}
}
Err(err) => eprintln!("[WARNING] Could not resolve callback host to an IP address: {err}"),
};
let listen_port = args.local_port.unwrap_or(callback_port);
Ok(Params {
callback: args.callback.to_string(),
listen: SocketAddr::new(UNSPECIFIED_IP, listen_port),
})
}
fn print_with_trailing_newline(data: &[u8]) {
print!("{}", String::from_utf8_lossy(data));
if !data.ends_with("\n".as_bytes()) {
println!();
}
}
const CMD_EXIT: &str = "exit";
static CMD_EXIT_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(&format!("(?i)\\s*{CMD_EXIT}\\s*")).unwrap());
enum ReplInput {
Command(String),
Interrupted,
}
async fn ask_user_next_command(
cmd_history: &mut Vec<String>,
sigint_manager: &mut sigint::Manager,
) -> Result<ReplInput, String> {
let (mut readline, rl_writer) = Readline::new(">> ".to_string())
.map_err(|err| format!("Could not set up input prompt: {err}"))?;
for cmd in &mut *cmd_history {
readline.add_history_entry((*cmd).to_string());
}
let suppress_sigint = sigint_manager.suppress();
let readline_result = readline.readline().await;
suppress_sigint.release();
readline.flush().unwrap();
drop(rl_writer);
match readline_result {
Ok(ReadlineEvent::Line(command)) => Ok(ReplInput::Command(command)),
Ok(ReadlineEvent::Eof | ReadlineEvent::Interrupted) => Ok(ReplInput::Interrupted),
Err(err) => Err(format!("Couldn't read input: {err}")),
}
}
async fn hangup(conn: Connection) -> Result<(), String> {
conn.tx
.send(SecureMsg::ExitBreak(ExitBreak {}))
.await
.map_err(|err| format!("Could not tell breakpoint to exit: {err}"))?;
Ok(())
}
enum AfterSession {
Iterate,
Exit,
}
async fn run_session(
mut conn: Connection,
cmd_history: &mut Vec<String>,
sigint_manager: &mut sigint::Manager,
) -> Result<AfterSession, String> {
println!("Session ready. You can enter single-line commands. Use `{CMD_EXIT}` to exit.");
let mut cmd_seq = 0;
loop {
let command: Option<String> =
match ask_user_next_command(cmd_history, sigint_manager).await? {
ReplInput::Command(command) => {
if command.is_empty() {
continue; }
if CMD_EXIT_PATTERN.is_match(&command) {
None
} else {
Some(command)
}
}
ReplInput::Interrupted => None,
};
let Some(command) = command else {
match ask_user_exit_choice().await {
ExitChoice::Resume => {
continue;
}
ExitChoice::Iterate => {
hangup(conn).await?;
return Ok(AfterSession::Iterate);
}
ExitChoice::Exit => {
hangup(conn).await?;
return Ok(AfterSession::Exit);
}
}
};
if !command.starts_with(' ') && Some(&command) != cmd_history.last() {
cmd_history.push(command.clone());
}
conn.tx
.send(SecureMsg::Command(Command {
command: command.clone(),
seq: cmd_seq,
}))
.await
.map_err(|err| format!("Could not send command to breakpoint: {err}"))?;
let mut killing = false;
loop {
tokio::select! {
biased;
_ = signal::ctrl_c() => {
if !killing {
println!(); println!("Attempting to terminate command...");
conn.tx.send(
SecureMsg::TerminateCmd(TerminateCmd { seq: cmd_seq })
).await.map_err(|err| format!("Could not send termination command to breakpoint: {err}"))?;
killing = true;
}
},
msg = conn.rx.recv() => match msg.ok_or("Error reading from breakpoint.")? {
SecureMsg::OutputLine(output) => {
if output.seq != cmd_seq {
return Err(format!(
"Received output for command #{} instead of #{}",
output.seq, cmd_seq
));
}
let log_tag = match output.gender {
OutputType::Stdout => "out",
OutputType::Stderr => "err",
};
print!("[{log_tag}] ");
print_with_trailing_newline(&output.bytes);
}
SecureMsg::Finished(outcome) => {
if outcome.seq != cmd_seq {
return Err(format!(
"Received completion notification for command #{} instead of #{}",
outcome.seq, cmd_seq
));
}
match outcome.exit_code {
Some(status) => println!("[exit: {status}]"),
None => println!("[exit: terminated by signal]"),
};
break;
}
_ => Err("Received unexpected message type from breakpoint")?,
},
};
}
cmd_seq += 1;
}
}
enum ExitChoice {
Resume,
Iterate,
Exit,
}
async fn ask_user_exit_choice() -> ExitChoice {
println!(indoc! {
"You've left the command shell. Do you want to resume, or exit?
[1] Resume: Go back to issuing commands to the breakpoint.
[2] Iterate: Exit from breakpoint on remote end, running script normally, but leave controller running with same keys.
[3] Exit: Exit from breakpoint and controller. For additional debugging you'll need to provision the breakpoint with a new key and secret."
});
println!();
let (mut readline, mut rl_writer) = Readline::new("Choice [1/2/3]: ".to_string()).unwrap();
let choice = loop {
match readline.readline().await {
Ok(ReadlineEvent::Line(choice)) => match choice.as_str() {
"1" => break ExitChoice::Resume,
"2" => break ExitChoice::Iterate,
"3" => break ExitChoice::Exit,
_ => continue,
},
Ok(ReadlineEvent::Eof) => continue,
Ok(ReadlineEvent::Interrupted) => break ExitChoice::Exit,
Err(err) => {
rl_writer
.write_all(format!("Failed to read input, exiting: {err}\n").as_bytes())
.unwrap();
break ExitChoice::Exit;
}
};
};
readline.flush().unwrap(); choice
}
fn print_connection_instructions(
setup_secret: &SetupSecret,
controller_pubkey: &X25519::PublicKey,
params: &Params,
) {
let setup_secret_b64 = &BASE64_STANDARD_NO_PAD.encode(setup_secret.as_slice());
let controller_pubkey_b64 = &BASE64_STANDARD_NO_PAD.encode(controller_pubkey.as_slice());
println!(
indoc! {r#"On the host where you want to set a breakpoint, set the following environment variable in a secure way:
BM_SETUP_SECRET={}
(Warning: Anyone who can see the secret can connect to you and pretend to be the breakpoint system, so setting it in an encrypted vault or secret store is recommended. However, they will not be able to impersonate you to the breakpoint system.)
Then, ensure the breakpoint end runs the following command from the environment you want to inspect:
breakmancer break -c {} -i {}
You can add --which="some location" to distinguish between breakpoints.
(Now listening on {} for callback...)"#},
setup_secret_b64, ¶ms.callback, &controller_pubkey_b64, ¶ms.listen,
);
}
pub async fn run(args: &Cli) -> Result<(), String> {
let params = parse_args(args)?;
let (controller_keypair, setup_secret) = match crate::debug_utils::get_debug_keys() {
Some(keys) => (keys.controller_keypair, keys.setup_secret),
None => (
X25519::Keypair::generate().unwrap(),
SetupSecret::new_random(),
),
};
let mut sigint_manager = sigint::Manager::setup();
let server_socket = TcpListener::bind(params.listen)
.await
.map_err(|err| format!("Could not listen on {}: {}", ¶ms.listen, err))?;
print_connection_instructions(&setup_secret, &controller_keypair.public_key, ¶ms);
let mut command_history = Vec::<String>::new();
loop {
let backoff = Duration::from_secs(10);
let (conn, breakpoint_intro) = match Connection::new_controller(
&server_socket,
&controller_keypair,
&setup_secret,
)
.await
{
Ok(conn) => conn,
Err(err) => {
println!(
"Failed to start controller connection; waiting {backoff:?} and trying again: {err}"
);
tokio::time::sleep(backoff).await;
continue;
}
};
if let Some(which) = breakpoint_intro.which {
println!("Breakpoint: {which}");
}
match run_session(conn, &mut command_history, &mut sigint_manager).await {
Ok(AfterSession::Iterate) => {
println!("Will wait for new connection...");
}
Ok(AfterSession::Exit) => {
return Ok(());
}
Err(err) => {
println!("Encountered error, restarting connection: {err}");
}
}
}
}