1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! Breakpoint side of the protocol.
use std::{process::Stdio, time::Duration};
use alkali::asymmetric::kx::x25519blake2b as X25519;
use base64::prelude::{Engine as _, BASE64_STANDARD_NO_PAD};
use chrono::Utc;
use clap::Parser;
use tokio::{
io::{AsyncBufReadExt, AsyncRead, BufReader},
process::Command,
sync::mpsc::{self, Receiver, Sender},
time::{self, sleep},
};
use crate::{
protocol::{
Command as CommandMsg, Connection, Finished, OobSecret, OutputLine, OutputType, SecureMsg,
},
util,
};
#[derive(Parser, Debug)]
pub struct Cli {
/// Callback address and port.
///
/// IPV6 addresses must be enclosed in `[square brackets]`.
#[arg(required = true, long, short = 'c', value_name = "HOST:PORT")]
callback: String,
/// Secret for encrypted channel.
///
/// It is strongly advised to pass this via environment variable,
/// as process arguments are generally visible to all users on a
/// system.
///
/// The nature of this secret depends on the protocol version.
#[arg(required = true, long, env = "BM_CALLBACK_SECRET")]
secret: String,
/// Listener's identity.
///
/// Identity of the party initiating the session. This is used in
/// setting up the encrypted channel, and the exact nature of the
/// value depends on the protocol version.
#[arg(required = true, long, short = 'i')]
listener: String,
/// Which breakpoint is this?
///
/// Any identifying string to help distinguish between multiple
/// breakpoints.
#[arg(long, short = 'w')]
which: Option<String>,
}
struct Params {
callback: String,
oob_secret: OobSecret,
listener_public: X25519::PublicKey,
which: Option<String>,
}
fn parse_args(args: &Cli) -> Result<Params, String> {
util::parse_address(&args.callback)
.map_err(|err| format!("Invalid callback address: {err}"))?;
let oob_secret_bytes = BASE64_STANDARD_NO_PAD
.decode(&args.secret)
.map_err(|err| format!("Could not parse secret argument as Base64: {err}"))?;
let oob_secret = OobSecret::from_bytes(&oob_secret_bytes);
let listen_pub = BASE64_STANDARD_NO_PAD
.decode(&args.listener)
.map_err(|err| format!("Could not parse listener argument as Base64: {err}"))?;
let listen_pub = <[u8; X25519::PUBLIC_KEY_LENGTH]>::try_from(listen_pub).map_err(|_| {
format!(
"Listener identity must be {} bytes of data",
X25519::PUBLIC_KEY_LENGTH
)
})?;
Ok(Params {
callback: args.callback.to_string(),
oob_secret,
listener_public: listen_pub,
which: args.which.clone(),
})
}
fn log(msg: &str) {
let timestamp = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
println!("[{timestamp}] {msg}");
}
// Spins up a thread that reads from the subprocess stdout or
// stderr and sends lines to a channel for sending over the wire.
fn spool_output<T>(source: T, output_type: OutputType, sink: Sender<SecureMsg>, seq: u32)
where
T: AsyncRead + Send + Unpin + 'static,
{
tokio::spawn(async move {
let mut reader = BufReader::new(source);
loop {
let mut buf = Vec::new();
let num_read = reader.read_until(0x0A, &mut buf).await.unwrap();
if num_read == 0 {
return;
}
let out_msg = SecureMsg::OutputLine(OutputLine {
bytes: buf,
gender: output_type.clone(),
seq,
});
if sink.send(out_msg).await.is_err() {
// Message transmitter has closed early (child
// process has probably been killed).
return;
}
}
});
}
/// Execute the command and send its results to the listener.
async fn execute_and_stream_results(
conn: &mut Connection,
command_msg: &CommandMsg,
) -> Result<(), String> {
let mut child = Command::new("bash")
.arg("-c")
.arg(&command_msg.command)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let child_out = child.stdout.take().unwrap();
let child_err = child.stderr.take().unwrap();
// We'll read stdout and stderr from the process in separate
// threads and send them a line at a time into this channel; a
// loop will receive both and actually send them over the wire.
//
// `Connection` itself can't be used by the threads, since the
// cipher_stream structs are !Send.
let (stdout_producer, mut msg_consumer): (Sender<SecureMsg>, Receiver<SecureMsg>) =
mpsc::channel(32);
let stderr_producer = stdout_producer.clone();
let seq = command_msg.seq;
// Each of these threads reads stdout or stderr until EOF and then
// exits (and drops the Sender).
//
// TODO: Consider whether this is the ideal approach for reading
// stdout and stderr. There's no inherent way to keep them in
// sync. Reading each one as fast as possible may help, but if one
// is way more active than the other then it may get desynced. If
// the child process only emits full lines to one at a time, then
// keeping a channel with a buffer capacity of 1 would be ideal --
// but it might make a different situation worse.
spool_output(child_out, OutputType::Stdout, stdout_producer, seq);
spool_output(child_err, OutputType::Stderr, stderr_producer, seq);
// Limit how fast we spool out the output. This is important for
// not flooding the connection in case a termination command comes
// down the line.
let mut interval = time::interval(Duration::from_millis(10));
interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay);
loop {
tokio::select! {
// Always check the termination branch first, to ensure it
// is handled on a fast stream.
biased;
// The listener might ask us to terminate the command.
listener_msg = conn.rx.recv() => match listener_msg {
None => {
// Connection has been lost, so kill the child
// process before exiting (since the listener will
// no longer have control over it.)
log("[DEBUG] Attempting to terminate the command due to lost connection...");
child.kill().await.map_err(|err| format!(
"Failed to kill child process after connection was lost: {err}"
))?;
Err("Lost connection while running command.")?;
},
Some(SecureMsg::TerminateCmd(term)) => {
if term.seq != seq {
Err(format!(
"Listener requested termination of command #{} but we're running command #{}.",
term.seq, seq
))?;
}
log("[DEBUG] Attempting to terminate the command at the listener's request...");
child.kill().await.map_err(|err| format!(
"Failed to kill child process at listener's request: {err}"
))?;
log("Done.");
break;
},
Some(_) => {
// TODO: More informative (include type of message)
Err("Unexpected message from listener while running command")?;
},
},
// The main stdout/stderr-sending branch.
return_msg_maybe = msg_consumer.recv() => match return_msg_maybe {
Some(msg) => conn.tx.send(msg)
.await
.map_err(|err| format!("Could not send command output to listener: {err}"))?,
None => break,
},
}
interval.tick().await;
}
// TODO: Check whether this is correct -- should the wait happen
// earlier? Or are we guaranteed the process has already exited
// anyhow, since stdout and stderr have reached EOF?
let exit_status = child.wait_with_output().await.unwrap();
// Some debug code to see if this situation ever happens.
let extra_stdout = exit_status.stdout.len();
let extra_stderr = exit_status.stderr.len();
if extra_stdout > 0 || extra_stderr > 0 {
eprintln!(
"Possible bug: There was still data in stdout ({extra_stdout} bytes) or stderr ({extra_stderr} bytes) by the time we called wait on the process."
);
}
// Send exit code and let listener know the process finished.
conn.tx
.send(SecureMsg::Finished(Finished {
exit_code: exit_status.status.code(),
seq,
}))
.await
.map_err(|err| format!("Could not send command exit code to listener: {err}"))
}
/// What to do after the connection ends.
struct AfterDisconnect {
reconnect: bool,
}
async fn run_session(conn: &mut Connection) -> Result<AfterDisconnect, String> {
log("Waiting for first command...");
loop {
let command_msg = match conn
.rx
.recv()
.await
.ok_or("Listener no longer sending messages")?
{
SecureMsg::Command(msg) => msg,
SecureMsg::ExitBreak(_) => {
eprintln!("Listener asked for normal execution to resume; exiting breakpoint.");
return Ok(AfterDisconnect { reconnect: false });
}
// TODO: Info about what kind of message was received
_ => Err("Received unexpected message from listener")?,
};
log(&format!("Running command: {}", command_msg.command));
execute_and_stream_results(conn, &command_msg).await?;
}
}
pub async fn run(args: &Cli) -> Result<(), String> {
let params = parse_args(args)?;
let backoff = time::Duration::from_secs(10);
loop {
// Allow override by debug keys
//
// Note that we create a new breakpoint keypair on every
// iteration, just on the principle of clearing as much stale
// state as possible.
let (oob_secret, listener_public, breakpoint_keypair) =
match crate::debug_utils::get_debug_keys() {
Some(keys) => (
keys.oob_secret,
keys.listener_keypair.public_key,
keys.breakpoint_keypair,
),
None => (
// TODO: Terrible, do something smarter.
OobSecret::from_bytes(params.oob_secret.as_slice()),
params.listener_public,
X25519::Keypair::generate().unwrap(),
),
};
let mut conn = match Connection::new_breakpoint(
&breakpoint_keypair,
¶ms.callback.to_string(),
&listener_public,
&oob_secret,
¶ms.which,
)
.await
{
Ok(conn) => conn,
Err(err) => {
log(&format!(
"Failed to connect to listener; waiting {backoff:?} and trying again: {err}"
));
sleep(backoff).await;
continue;
}
};
match run_session(&mut conn).await {
Ok(after) => {
// This branch is always taken, which is a little odd,
// but probably better to have it explicit.
if !after.reconnect {
return Ok(());
}
}
Err(err) => log(&format!("Encountered error, restarting connection: {err}")),
}
}
}