Implement PTY-native session terminal control
This commit is contained in:
parent
058e58e816
commit
8cef037fa3
25 changed files with 1488 additions and 382 deletions
|
|
@ -1,8 +1,10 @@
|
|||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Write};
|
||||
use std::net::Shutdown;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{Mutex, mpsc};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
|
|
@ -10,8 +12,8 @@ use lazaret_control_client::{ControlClient, ControlClientError, PendingClientEnr
|
|||
use lazaret_core::identity::{ClientId, SessionAnchorId, SessionId};
|
||||
use lazaret_core::records::{
|
||||
ClientEnrollmentResponse, ControlRequest, ControlResponse, DeveloperDecision,
|
||||
DeveloperDecisionKind, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionRequest, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||
DeveloperDecisionKind, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||
InteractiveSessionRequestV2, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||
ProtocolPublicationIdentity, ProtocolVersion, SessionBinding, SessionExitOutcome,
|
||||
SessionRegistrationRequest, TlsSubjectId,
|
||||
};
|
||||
|
|
@ -414,9 +416,9 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
|||
let cli_id = identity.cli_id;
|
||||
let session_id = identity.session_id;
|
||||
let pending = PendingClientEnrollment::create(generation.clone(), cli_id)?;
|
||||
let anonymous = ControlClient::config(&endpoint.publication, None)?;
|
||||
let anonymous = ControlClient::config_v2(&endpoint.publication, None)?;
|
||||
let mut enrollment =
|
||||
ControlClient::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
|
||||
ControlClient::connect_v2(&endpoint.socket_path, anonymous, &endpoint.publication)?;
|
||||
ControlClient::send(
|
||||
&mut enrollment,
|
||||
&ControlRequest::Enroll(pending.request().clone()),
|
||||
|
|
@ -442,12 +444,12 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
|||
session_id,
|
||||
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
|
||||
};
|
||||
let authenticated = ControlClient::config(
|
||||
let authenticated = ControlClient::config_v2(
|
||||
&endpoint.publication,
|
||||
Some((&credential, pending.private_key())),
|
||||
)?;
|
||||
let mut stream =
|
||||
ControlClient::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
|
||||
ControlClient::connect_v2(&endpoint.socket_path, authenticated, &endpoint.publication)?;
|
||||
ControlClient::send(
|
||||
&mut stream,
|
||||
&ControlRequest::Register(SessionRegistrationRequest {
|
||||
|
|
@ -466,13 +468,70 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
|||
));
|
||||
}
|
||||
endpoint.acknowledge_waiter_transfer()?;
|
||||
let mut transport = ControlFaceInteractiveTransport { stream };
|
||||
let mut transport = ControlFaceInteractiveTransport::new(stream).map_err(widen_client_error)?;
|
||||
run_interactive_session(&mut transport, terminal, identity, &binding, argv)
|
||||
.map_err(widen_client_error)
|
||||
}
|
||||
|
||||
struct ControlFaceInteractiveTransport {
|
||||
stream: rustls::StreamOwned<rustls::ClientConnection, std::os::unix::net::UnixStream>,
|
||||
commands: Option<mpsc::Sender<InteractiveTransportCommand>>,
|
||||
batches:
|
||||
mpsc::Receiver<Result<InteractiveSessionBatchV2, ClientError<std::convert::Infallible>>>,
|
||||
cancellation: std::os::unix::net::UnixStream,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
enum InteractiveTransportCommand {
|
||||
Start(InteractiveSessionRequestV2),
|
||||
Event(InteractiveSessionEventV2),
|
||||
}
|
||||
|
||||
impl ControlFaceInteractiveTransport {
|
||||
fn new(
|
||||
mut stream: rustls::StreamOwned<rustls::ClientConnection, std::os::unix::net::UnixStream>,
|
||||
) -> Result<Self, ClientError<std::convert::Infallible>> {
|
||||
let cancellation = stream.sock.try_clone()?;
|
||||
let (commands, command_rx) = mpsc::channel();
|
||||
let (batch_tx, batches) = mpsc::channel();
|
||||
let worker = std::thread::spawn(move || {
|
||||
while let Ok(command) = command_rx.recv() {
|
||||
let result = match command {
|
||||
InteractiveTransportCommand::Start(request) => ControlClient::send(
|
||||
&mut stream,
|
||||
&ControlRequest::ExecuteInteractiveV2(request),
|
||||
)
|
||||
.map_err(map_control_connection)
|
||||
.and_then(|()| receive_interactive_batch(&mut stream)),
|
||||
InteractiveTransportCommand::Event(event) => {
|
||||
ControlClient::send(&mut stream, &event)
|
||||
.map_err(map_control_connection)
|
||||
.and_then(|()| receive_interactive_batch(&mut stream))
|
||||
}
|
||||
};
|
||||
let failed = result.is_err();
|
||||
if batch_tx.send(result).is_err() || failed {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(Self {
|
||||
commands: Some(commands),
|
||||
batches,
|
||||
cancellation,
|
||||
worker: Some(worker),
|
||||
})
|
||||
}
|
||||
|
||||
fn submit(
|
||||
&self,
|
||||
command: InteractiveTransportCommand,
|
||||
) -> Result<(), ClientError<std::convert::Infallible>> {
|
||||
self.commands
|
||||
.as_ref()
|
||||
.ok_or(ClientError::LostConnection)?
|
||||
.send(command)
|
||||
.map_err(|_| ClientError::LostConnection)
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
|
||||
|
|
@ -480,27 +539,43 @@ impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
|
|||
|
||||
fn start(
|
||||
&mut self,
|
||||
request: InteractiveSessionRequest,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
ControlClient::send(
|
||||
&mut self.stream,
|
||||
&ControlRequest::ExecuteInteractive(request),
|
||||
)?;
|
||||
receive_interactive_batch(&mut self.stream)
|
||||
request: InteractiveSessionRequestV2,
|
||||
) -> Result<(), ClientError<Self::Error>> {
|
||||
self.submit(InteractiveTransportCommand::Start(request))
|
||||
}
|
||||
|
||||
fn exchange(
|
||||
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>> {
|
||||
self.submit(InteractiveTransportCommand::Event(event))
|
||||
}
|
||||
|
||||
fn try_receive(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
ControlClient::send(&mut self.stream, &event).map_err(map_control_connection)?;
|
||||
receive_interactive_batch(&mut self.stream)
|
||||
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>> {
|
||||
match self.batches.try_recv() {
|
||||
Ok(batch) => batch.map(Some),
|
||||
Err(mpsc::TryRecvError::Empty) => Ok(None),
|
||||
Err(mpsc::TryRecvError::Disconnected) => Err(ClientError::LostConnection),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
self.commands.take();
|
||||
let _ = self.cancellation.shutdown(Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ControlFaceInteractiveTransport {
|
||||
fn drop(&mut self) {
|
||||
self.cancel();
|
||||
if let Some(worker) = self.worker.take() {
|
||||
let _ = worker.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_interactive_batch(
|
||||
stream: &mut impl std::io::Read,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<std::convert::Infallible>> {
|
||||
) -> Result<InteractiveSessionBatchV2, ClientError<std::convert::Infallible>> {
|
||||
ControlClient::receive(stream).map_err(map_control_connection)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::num::NonZeroU16;
|
||||
use std::time::Duration;
|
||||
|
||||
use lazaret_core::records::{
|
||||
DeveloperDecisionKind, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionMessage, InteractiveSessionRequest, ProtocolVersion, SessionBinding,
|
||||
SessionClose, SessionExitOutcome, SessionTerminalEvent, SessionTerminalMessage, TerminalInput,
|
||||
TerminalPrompt, TerminalResize, TerminalSignal, TerminalSignalKind,
|
||||
DeveloperDecisionKind, InteractiveProtocolVersion, InteractiveSessionBatchV2,
|
||||
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequestV2,
|
||||
SessionBinding, SessionExitOutcome, SessionTerminalEventV2, SessionTerminalMessage,
|
||||
TerminalBreak, TerminalInput, TerminalModesV2, TerminalPrompt, TerminalResize,
|
||||
};
|
||||
use nix::sys::signal::{SigSet, SigmaskHow, Signal};
|
||||
use nix::sys::signalfd::{SfdFlags, SignalFd};
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||
use rustix::termios::{LocalModes, OptionalActions, Termios, tcgetattr, tcgetwinsize, tcsetattr};
|
||||
use rustix::termios::{
|
||||
OptionalActions, SpecialCodeIndex, Termios, tcgetattr, tcgetwinsize, tcsetattr,
|
||||
};
|
||||
|
||||
use crate::{ClientError, ClientSessionIdentity};
|
||||
|
||||
|
|
@ -23,21 +26,26 @@ pub trait InteractiveSessionTransport {
|
|||
/// Returns an authenticated session or framing failure.
|
||||
fn start(
|
||||
&mut self,
|
||||
request: InteractiveSessionRequest,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>>;
|
||||
request: InteractiveSessionRequestV2,
|
||||
) -> Result<(), ClientError<Self::Error>>;
|
||||
|
||||
/// # Errors
|
||||
/// Returns a lost connection, stale session, or framing failure.
|
||||
fn exchange(
|
||||
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>>;
|
||||
|
||||
/// # Errors
|
||||
/// Returns a lost connection, stale session, or framing failure.
|
||||
fn try_receive(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>>;
|
||||
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>>;
|
||||
|
||||
fn cancel(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum HostTerminalEvent {
|
||||
Input(Vec<u8>),
|
||||
Signal(TerminalSignalKind),
|
||||
Break,
|
||||
Resize,
|
||||
LocalSuspend,
|
||||
Terminate,
|
||||
|
|
@ -54,6 +62,7 @@ pub trait HostTerminal {
|
|||
/// # Errors
|
||||
/// Returns an error when the current terminal size cannot be observed.
|
||||
fn size(&self, session_id: lazaret_core::identity::SessionId) -> io::Result<TerminalResize>;
|
||||
fn modes(&self) -> TerminalModesV2;
|
||||
/// # Errors
|
||||
/// Returns an error while observing terminal input or signals.
|
||||
fn next_event(&mut self) -> io::Result<HostTerminalEvent>;
|
||||
|
|
@ -80,6 +89,9 @@ pub struct SystemTerminal {
|
|||
original_flags: OFlags,
|
||||
raw: bool,
|
||||
signals: SignalQueue,
|
||||
events: VecDeque<HostTerminalEvent>,
|
||||
line_start: bool,
|
||||
escape_pending: bool,
|
||||
}
|
||||
|
||||
impl SystemTerminal {
|
||||
|
|
@ -95,8 +107,59 @@ impl SystemTerminal {
|
|||
terminal,
|
||||
raw: false,
|
||||
signals: SignalQueue::register()?,
|
||||
events: VecDeque::new(),
|
||||
line_start: true,
|
||||
escape_pending: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn queue_input(&mut self, bytes: &[u8]) {
|
||||
let mut forwarded = Vec::new();
|
||||
let mut events = VecDeque::new();
|
||||
for &byte in bytes {
|
||||
if self.escape_pending {
|
||||
self.escape_pending = false;
|
||||
match byte {
|
||||
b'.' | 0x1a | b'B' => {
|
||||
if !forwarded.is_empty() {
|
||||
events.push_back(HostTerminalEvent::Input(std::mem::take(
|
||||
&mut forwarded,
|
||||
)));
|
||||
}
|
||||
events.push_back(match byte {
|
||||
b'.' => HostTerminalEvent::Terminate,
|
||||
0x1a => HostTerminalEvent::LocalSuspend,
|
||||
b'B' => HostTerminalEvent::Break,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
self.line_start = true;
|
||||
continue;
|
||||
}
|
||||
b'~' => {
|
||||
forwarded.push(b'~');
|
||||
self.line_start = false;
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
forwarded.push(b'~');
|
||||
forwarded.push(byte);
|
||||
}
|
||||
}
|
||||
self.line_start = matches!(byte, b'\r' | b'\n');
|
||||
continue;
|
||||
}
|
||||
if self.line_start && byte == b'~' {
|
||||
self.escape_pending = true;
|
||||
continue;
|
||||
}
|
||||
forwarded.push(byte);
|
||||
self.line_start = matches!(byte, b'\r' | b'\n');
|
||||
}
|
||||
if !forwarded.is_empty() {
|
||||
events.push_back(HostTerminalEvent::Input(forwarded));
|
||||
}
|
||||
self.events.extend(events);
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTerminal for SystemTerminal {
|
||||
|
|
@ -106,7 +169,6 @@ impl HostTerminal for SystemTerminal {
|
|||
}
|
||||
let mut raw = self.original.clone();
|
||||
raw.make_raw();
|
||||
raw.local_modes.insert(LocalModes::ISIG);
|
||||
tcsetattr(&self.terminal, OptionalActions::Flush, &raw)?;
|
||||
if let Err(error) = fcntl_setfl(&self.terminal, self.original_flags | OFlags::NONBLOCK) {
|
||||
let _ = tcsetattr(&self.terminal, OptionalActions::Flush, &self.original);
|
||||
|
|
@ -139,7 +201,14 @@ impl HostTerminal for SystemTerminal {
|
|||
})
|
||||
}
|
||||
|
||||
fn modes(&self) -> TerminalModesV2 {
|
||||
terminal_modes_v2(&self.original)
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> io::Result<HostTerminalEvent> {
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Ok(event);
|
||||
}
|
||||
if let Some(event) = self.signals.next_event()? {
|
||||
return Ok(event);
|
||||
}
|
||||
|
|
@ -148,7 +217,8 @@ impl HostTerminal for SystemTerminal {
|
|||
Ok(0) => Ok(HostTerminalEvent::Terminate),
|
||||
Ok(length) => {
|
||||
bytes.truncate(length);
|
||||
Ok(HostTerminalEvent::Input(bytes))
|
||||
self.queue_input(&bytes);
|
||||
Ok(self.events.pop_front().unwrap_or(HostTerminalEvent::Idle))
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
|
|
@ -196,9 +266,7 @@ impl HostTerminal for SystemTerminal {
|
|||
resumed_size = Some(self.size(prompt.session_id)?);
|
||||
}
|
||||
HostTerminalEvent::Terminate => return Err(io::ErrorKind::Interrupted.into()),
|
||||
HostTerminalEvent::Idle
|
||||
| HostTerminalEvent::Resize
|
||||
| HostTerminalEvent::Signal(_) => {}
|
||||
HostTerminalEvent::Idle | HostTerminalEvent::Resize | HostTerminalEvent::Break => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -259,11 +327,11 @@ impl SignalQueue {
|
|||
};
|
||||
let signal = Signal::try_from(info.ssi_signo.cast_signed()).map_err(errno_to_io)?;
|
||||
Ok(match signal {
|
||||
Signal::SIGINT => Some(HostTerminalEvent::Signal(TerminalSignalKind::Interrupt)),
|
||||
Signal::SIGQUIT => Some(HostTerminalEvent::Signal(TerminalSignalKind::Quit)),
|
||||
Signal::SIGINT | Signal::SIGQUIT | Signal::SIGTERM | Signal::SIGHUP => {
|
||||
Some(HostTerminalEvent::Terminate)
|
||||
}
|
||||
Signal::SIGTSTP => Some(HostTerminalEvent::LocalSuspend),
|
||||
Signal::SIGWINCH => Some(HostTerminalEvent::Resize),
|
||||
Signal::SIGTERM | Signal::SIGHUP => Some(HostTerminalEvent::Terminate),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
|
@ -282,6 +350,35 @@ impl Drop for SignalQueue {
|
|||
}
|
||||
}
|
||||
|
||||
fn terminal_modes_v2(modes: &Termios) -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: modes.input_modes.bits(),
|
||||
output_modes: modes.output_modes.bits(),
|
||||
control_modes: modes.control_modes.bits(),
|
||||
local_modes: modes.local_modes.bits(),
|
||||
line_discipline: modes.line_discipline,
|
||||
input_speed: modes.input_speed(),
|
||||
output_speed: modes.output_speed(),
|
||||
interrupt: modes.special_codes[SpecialCodeIndex::VINTR],
|
||||
quit: modes.special_codes[SpecialCodeIndex::VQUIT],
|
||||
erase: modes.special_codes[SpecialCodeIndex::VERASE],
|
||||
kill: modes.special_codes[SpecialCodeIndex::VKILL],
|
||||
eof: modes.special_codes[SpecialCodeIndex::VEOF],
|
||||
time: modes.special_codes[SpecialCodeIndex::VTIME],
|
||||
minimum: modes.special_codes[SpecialCodeIndex::VMIN],
|
||||
switch: modes.special_codes[SpecialCodeIndex::VSWTC],
|
||||
start: modes.special_codes[SpecialCodeIndex::VSTART],
|
||||
stop: modes.special_codes[SpecialCodeIndex::VSTOP],
|
||||
suspend: modes.special_codes[SpecialCodeIndex::VSUSP],
|
||||
end_of_line: modes.special_codes[SpecialCodeIndex::VEOL],
|
||||
reprint: modes.special_codes[SpecialCodeIndex::VREPRINT],
|
||||
discard: modes.special_codes[SpecialCodeIndex::VDISCARD],
|
||||
word_erase: modes.special_codes[SpecialCodeIndex::VWERASE],
|
||||
literal_next: modes.special_codes[SpecialCodeIndex::VLNEXT],
|
||||
end_of_line_2: modes.special_codes[SpecialCodeIndex::VEOL2],
|
||||
}
|
||||
}
|
||||
|
||||
fn errno_to_io(error: nix::errno::Errno) -> io::Error {
|
||||
io::Error::from_raw_os_error(error as i32)
|
||||
}
|
||||
|
|
@ -301,14 +398,18 @@ pub fn run_interactive_session<T: InteractiveSessionTransport, H: HostTerminal>(
|
|||
let result = (|| {
|
||||
let session_id = identity.session_id();
|
||||
let initial_size = terminal.size(session_id)?;
|
||||
let mut batch = transport.start(InteractiveSessionRequest {
|
||||
protocol_version: ProtocolVersion::V1,
|
||||
transport.start(InteractiveSessionRequestV2 {
|
||||
protocol_version: InteractiveProtocolVersion::V2,
|
||||
session_id,
|
||||
argv: argv.to_vec(),
|
||||
initial_size,
|
||||
initial_modes: terminal.modes(),
|
||||
})?;
|
||||
let mut pending = VecDeque::new();
|
||||
loop {
|
||||
if let Some(exit) = handle_batch(transport, terminal, identity, binding, batch)? {
|
||||
if let Some(batch) = transport.try_receive()? {
|
||||
let handled = handle_batch(terminal, identity, binding, batch)?;
|
||||
if let Some(exit) = handled.exit {
|
||||
return match exit.outcome() {
|
||||
SessionExitOutcome::Exited => exit
|
||||
.exit_code()
|
||||
|
|
@ -318,46 +419,60 @@ pub fn run_interactive_session<T: InteractiveSessionTransport, H: HostTerminal>(
|
|||
}
|
||||
};
|
||||
}
|
||||
let event =
|
||||
match terminal.next_event()? {
|
||||
HostTerminalEvent::Input(bytes) => InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Input(TerminalInput { session_id, bytes }),
|
||||
),
|
||||
HostTerminalEvent::Signal(signal) => InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Signal(TerminalSignal { session_id, signal }),
|
||||
),
|
||||
HostTerminalEvent::Resize => InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Resize(terminal.size(session_id)?),
|
||||
),
|
||||
HostTerminalEvent::LocalSuspend => {
|
||||
terminal.suspend_and_resume()?;
|
||||
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(
|
||||
terminal.size(session_id)?,
|
||||
))
|
||||
for event in handled.events {
|
||||
enqueue_event(&mut pending, event)?;
|
||||
}
|
||||
} else {
|
||||
let event = terminal.next_event()?;
|
||||
match host_event(terminal, session_id, event)? {
|
||||
HostEvent::Remote(event) => enqueue_event(&mut pending, event)?,
|
||||
HostEvent::Continue => {}
|
||||
HostEvent::Disconnect => {
|
||||
transport.cancel();
|
||||
return Err(ClientError::LostConnection);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let event = if let Some(event) = pending.pop_front() {
|
||||
event
|
||||
} else {
|
||||
let event = terminal.next_event()?;
|
||||
match host_event(terminal, session_id, event)? {
|
||||
HostEvent::Remote(event) => event,
|
||||
HostEvent::Continue => InteractiveSessionEventV2::Poll { session_id },
|
||||
HostEvent::Disconnect => {
|
||||
transport.cancel();
|
||||
return Err(ClientError::LostConnection);
|
||||
}
|
||||
}
|
||||
HostTerminalEvent::Terminate => InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Close(SessionClose { session_id }),
|
||||
),
|
||||
HostTerminalEvent::Idle => InteractiveSessionEvent::Poll { session_id },
|
||||
};
|
||||
batch = transport.exchange(event)?;
|
||||
transport.send(event)?;
|
||||
}
|
||||
})();
|
||||
if result.is_err() {
|
||||
transport.cancel();
|
||||
}
|
||||
let restoration = terminal.restore().map_err(ClientError::Io);
|
||||
restoration?;
|
||||
result
|
||||
}
|
||||
|
||||
fn handle_batch<T: InteractiveSessionTransport, H: HostTerminal>(
|
||||
transport: &mut T,
|
||||
struct HandledBatch {
|
||||
exit: Option<lazaret_core::records::SessionExit>,
|
||||
events: Vec<InteractiveSessionEventV2>,
|
||||
}
|
||||
|
||||
fn handle_batch<H: HostTerminal, E>(
|
||||
terminal: &mut H,
|
||||
identity: &ClientSessionIdentity,
|
||||
binding: &SessionBinding,
|
||||
batch: InteractiveSessionBatch,
|
||||
) -> Result<Option<lazaret_core::records::SessionExit>, ClientError<T::Error>> {
|
||||
batch: InteractiveSessionBatchV2,
|
||||
) -> Result<HandledBatch, ClientError<E>> {
|
||||
if batch.session_id != identity.session_id() {
|
||||
return Err(ClientError::Protocol("response Session ID differs"));
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
for message in batch.messages {
|
||||
match message {
|
||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Output(output)) => {
|
||||
|
|
@ -366,35 +481,92 @@ fn handle_batch<T: InteractiveSessionTransport, H: HostTerminal>(
|
|||
}
|
||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(exit)) => {
|
||||
require_session(exit.session_id(), identity.session_id())?;
|
||||
return Ok(Some(exit));
|
||||
return Ok(HandledBatch {
|
||||
exit: Some(exit),
|
||||
events,
|
||||
});
|
||||
}
|
||||
InteractiveSessionMessage::Prompt(prompt) => {
|
||||
require_session(prompt.session_id, identity.session_id())?;
|
||||
let outcome = terminal.prompt(&prompt)?;
|
||||
if let Some(resize) = outcome.resumed_size {
|
||||
let response = transport.exchange(InteractiveSessionEvent::Terminal(
|
||||
SessionTerminalEvent::Resize(resize),
|
||||
))?;
|
||||
if let Some(exit) =
|
||||
handle_batch(transport, terminal, identity, binding, response)?
|
||||
{
|
||||
return Ok(Some(exit));
|
||||
}
|
||||
events.push(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Resize(resize),
|
||||
));
|
||||
}
|
||||
let signed_decision =
|
||||
identity.sign_developer_decision(binding.clone(), &prompt, outcome.decision)?;
|
||||
let response = transport.exchange(InteractiveSessionEvent::PromptDecision {
|
||||
events.push(InteractiveSessionEventV2::PromptDecision {
|
||||
session_id: identity.session_id(),
|
||||
signed_decision,
|
||||
})?;
|
||||
if let Some(exit) = handle_batch(transport, terminal, identity, binding, response)?
|
||||
{
|
||||
return Ok(Some(exit));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(HandledBatch { exit: None, events })
|
||||
}
|
||||
|
||||
enum HostEvent {
|
||||
Remote(InteractiveSessionEventV2),
|
||||
Continue,
|
||||
Disconnect,
|
||||
}
|
||||
|
||||
fn host_event<H: HostTerminal, E>(
|
||||
terminal: &mut H,
|
||||
session_id: lazaret_core::identity::SessionId,
|
||||
event: HostTerminalEvent,
|
||||
) -> Result<HostEvent, ClientError<E>> {
|
||||
Ok(match event {
|
||||
HostTerminalEvent::Input(bytes) => HostEvent::Remote(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Input(TerminalInput { session_id, bytes }),
|
||||
)),
|
||||
HostTerminalEvent::Break => HostEvent::Remote(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Break(TerminalBreak { session_id }),
|
||||
)),
|
||||
HostTerminalEvent::Resize => HostEvent::Remote(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Resize(terminal.size(session_id)?),
|
||||
)),
|
||||
HostTerminalEvent::LocalSuspend => {
|
||||
terminal.suspend_and_resume()?;
|
||||
HostEvent::Remote(InteractiveSessionEventV2::Terminal(
|
||||
SessionTerminalEventV2::Resize(terminal.size(session_id)?),
|
||||
))
|
||||
}
|
||||
Ok(None)
|
||||
HostTerminalEvent::Terminate => HostEvent::Disconnect,
|
||||
HostTerminalEvent::Idle => HostEvent::Continue,
|
||||
})
|
||||
}
|
||||
|
||||
fn enqueue_event<E>(
|
||||
pending: &mut VecDeque<InteractiveSessionEventV2>,
|
||||
event: InteractiveSessionEventV2,
|
||||
) -> Result<(), ClientError<E>> {
|
||||
const MAX_PENDING_EVENTS: usize = 256;
|
||||
const MAX_PENDING_INPUT_BYTES: usize = 64 * 1024;
|
||||
|
||||
let bytes = pending
|
||||
.iter()
|
||||
.filter_map(|event| match event {
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Input(input)) => {
|
||||
Some(input.bytes.len())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.sum::<usize>()
|
||||
+ match &event {
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Input(input)) => {
|
||||
input.bytes.len()
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
if pending.len() >= MAX_PENDING_EVENTS || bytes > MAX_PENDING_INPUT_BYTES {
|
||||
return Err(ClientError::Io(io::Error::other(
|
||||
"interactive terminal transport backpressure",
|
||||
)));
|
||||
}
|
||||
pending.push_back(event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_session<E>(
|
||||
|
|
@ -426,10 +598,81 @@ fn hex(bytes: &[u8]) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::File;
|
||||
use std::io::Write as _;
|
||||
|
||||
use nix::sys::signal::{SigSet, SigmaskHow, Signal, kill};
|
||||
use nix::unistd::Pid;
|
||||
use rustix::fs::fcntl_getfl;
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{LocalModes, tcgetattr};
|
||||
|
||||
use super::SignalQueue;
|
||||
use super::{HostTerminal, HostTerminalEvent, SignalQueue, SystemTerminal};
|
||||
|
||||
fn terminal() -> (SystemTerminal, File) {
|
||||
let master =
|
||||
openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC).unwrap();
|
||||
grantpt(&master).unwrap();
|
||||
unlockpt(&master).unwrap();
|
||||
let slave = File::from(
|
||||
ioctl_tiocgptpeer(
|
||||
&master,
|
||||
OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let original = tcgetattr(&slave).unwrap();
|
||||
let original_flags = fcntl_getfl(&slave).unwrap();
|
||||
(
|
||||
SystemTerminal {
|
||||
terminal: slave,
|
||||
original,
|
||||
original_flags,
|
||||
raw: false,
|
||||
signals: SignalQueue::register().unwrap(),
|
||||
events: VecDeque::new(),
|
||||
line_start: true,
|
||||
escape_pending: false,
|
||||
},
|
||||
File::from(master),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_mode_forwards_control_bytes_without_local_signals() {
|
||||
let (mut terminal, mut master) = terminal();
|
||||
terminal.enter_raw().unwrap();
|
||||
assert!(
|
||||
!tcgetattr(&terminal.terminal)
|
||||
.unwrap()
|
||||
.local_modes
|
||||
.contains(LocalModes::ISIG)
|
||||
);
|
||||
master.write_all(&[3, 28, 26]).unwrap();
|
||||
assert_eq!(
|
||||
terminal.next_event().unwrap(),
|
||||
HostTerminalEvent::Input(vec![3, 28, 26])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_style_escapes_survive_read_boundaries_and_preserve_literals() {
|
||||
let (mut terminal, mut master) = terminal();
|
||||
terminal.enter_raw().unwrap();
|
||||
master.write_all(b"line\n~").unwrap();
|
||||
assert_eq!(
|
||||
terminal.next_event().unwrap(),
|
||||
HostTerminalEvent::Input(b"line\n".to_vec())
|
||||
);
|
||||
master.write_all(b"B~.~~literal\n").unwrap();
|
||||
assert_eq!(terminal.next_event().unwrap(), HostTerminalEvent::Break);
|
||||
assert_eq!(terminal.next_event().unwrap(), HostTerminalEvent::Terminate);
|
||||
assert_eq!(
|
||||
terminal.next_event().unwrap(),
|
||||
HostTerminalEvent::Input(b"~literal\n".to_vec())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ use lazaret_core::identity::{
|
|||
SandboxId, SessionId,
|
||||
};
|
||||
use lazaret_core::records::{
|
||||
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, SessionBinding,
|
||||
SessionExit, SessionTerminalMessage, SignedProtocolRecord, TerminalOutput, TerminalPrompt,
|
||||
TerminalResize, TerminalSignalKind,
|
||||
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatchV2,
|
||||
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequestV2,
|
||||
SessionBinding, SessionExit, SessionTerminalEventV2, SessionTerminalMessage,
|
||||
SignedProtocolRecord, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
};
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{LocalModes, OptionalActions, Termios, tcgetattr, tcsetattr};
|
||||
use rustix::termios::{OptionalActions, Termios, tcgetattr, tcsetattr};
|
||||
|
||||
struct PtyTerminal {
|
||||
_master: File,
|
||||
|
|
@ -74,7 +74,6 @@ impl HostTerminal for PtyTerminal {
|
|||
fn enter_raw(&mut self) -> io::Result<()> {
|
||||
let mut raw = self.original.clone();
|
||||
raw.make_raw();
|
||||
raw.local_modes.insert(LocalModes::ISIG);
|
||||
tcsetattr(&self.slave, OptionalActions::Now, &raw)?;
|
||||
fcntl_setfl(&self.slave, self.original_flags | OFlags::NONBLOCK)?;
|
||||
self.raw = true;
|
||||
|
|
@ -98,6 +97,10 @@ impl HostTerminal for PtyTerminal {
|
|||
})
|
||||
}
|
||||
|
||||
fn modes(&self) -> TerminalModesV2 {
|
||||
test_modes()
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> io::Result<HostTerminalEvent> {
|
||||
Ok(self.events.pop_front().unwrap_or(HostTerminalEvent::Idle))
|
||||
}
|
||||
|
|
@ -139,11 +142,14 @@ enum TransportFailure {
|
|||
|
||||
struct FixtureTransport {
|
||||
session_id: SessionId,
|
||||
events: Vec<InteractiveSessionEvent>,
|
||||
events: Vec<InteractiveSessionEventV2>,
|
||||
prompt_sent: bool,
|
||||
failure: Option<TransportFailure>,
|
||||
signed_decision: Option<Vec<u8>>,
|
||||
prompt_after_events: usize,
|
||||
batches: VecDeque<InteractiveSessionBatchV2>,
|
||||
cancelled: bool,
|
||||
blocked: bool,
|
||||
}
|
||||
|
||||
impl FixtureTransport {
|
||||
|
|
@ -155,6 +161,9 @@ impl FixtureTransport {
|
|||
failure: None,
|
||||
signed_decision: None,
|
||||
prompt_after_events: 5,
|
||||
batches: VecDeque::new(),
|
||||
cancelled: false,
|
||||
blocked: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +173,17 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
|
||||
fn start(
|
||||
&mut self,
|
||||
request: InteractiveSessionRequest,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
request: InteractiveSessionRequestV2,
|
||||
) -> Result<(), ClientError<Self::Error>> {
|
||||
if matches!(self.failure, Some(TransportFailure::Stale)) {
|
||||
return Err(ClientError::StaleCoordinator);
|
||||
}
|
||||
assert_eq!(request.session_id, self.session_id);
|
||||
Ok(batch(
|
||||
assert_eq!(
|
||||
request.protocol_version,
|
||||
lazaret_core::records::InteractiveProtocolVersion::V2
|
||||
);
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Terminal(
|
||||
SessionTerminalMessage::Output(TerminalOutput {
|
||||
|
|
@ -178,32 +191,31 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
bytes: b"guest-output".to_vec(),
|
||||
}),
|
||||
)],
|
||||
))
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
||||
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>> {
|
||||
if matches!(self.failure, Some(TransportFailure::Lost)) {
|
||||
return Err(ClientError::LostConnection);
|
||||
}
|
||||
self.events.push(event.clone());
|
||||
if let InteractiveSessionEvent::PromptDecision {
|
||||
if let InteractiveSessionEventV2::PromptDecision {
|
||||
signed_decision, ..
|
||||
} = event
|
||||
{
|
||||
self.signed_decision = Some(signed_decision);
|
||||
return Ok(batch(
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Terminal(
|
||||
SessionTerminalMessage::Exit(SessionExit::exited(self.session_id, 23)),
|
||||
)],
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
if !self.prompt_sent && self.events.len() >= self.prompt_after_events {
|
||||
self.prompt_sent = true;
|
||||
return Ok(batch(
|
||||
self.batches.push_back(batch(
|
||||
self.session_id,
|
||||
vec![InteractiveSessionMessage::Prompt(TerminalPrompt {
|
||||
session_id: self.session_id,
|
||||
|
|
@ -214,8 +226,23 @@ impl InteractiveSessionTransport for FixtureTransport {
|
|||
justification: "update host metadata".into(),
|
||||
})],
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
Ok(batch(self.session_id, Vec::new()))
|
||||
self.batches.push_back(batch(self.session_id, Vec::new()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_receive(
|
||||
&mut self,
|
||||
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>> {
|
||||
if self.blocked {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(self.batches.pop_front())
|
||||
}
|
||||
|
||||
fn cancel(&mut self) {
|
||||
self.cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -243,23 +270,21 @@ fn prompt_suspend_sends_resumed_size_before_decision() {
|
|||
assert!(matches!(
|
||||
transport.events.as_slice(),
|
||||
[
|
||||
InteractiveSessionEvent::Poll { .. },
|
||||
InteractiveSessionEvent::Terminal(lazaret_core::records::SessionTerminalEvent::Resize(
|
||||
_
|
||||
)),
|
||||
InteractiveSessionEvent::PromptDecision { .. }
|
||||
InteractiveSessionEventV2::Poll { .. },
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(_)),
|
||||
InteractiveSessionEventV2::PromptDecision { .. }
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_fixture_routes_resize_signals_suspend_prompt_and_nonzero_exit() {
|
||||
fn terminal_fixture_routes_resize_input_break_suspend_prompt_and_nonzero_exit() {
|
||||
let identity = ClientSessionIdentity::generate::<std::convert::Infallible>().unwrap();
|
||||
let binding = binding(&identity);
|
||||
let mut terminal = PtyTerminal::new([
|
||||
HostTerminalEvent::Resize,
|
||||
HostTerminalEvent::Signal(TerminalSignalKind::Interrupt),
|
||||
HostTerminalEvent::Signal(TerminalSignalKind::Quit),
|
||||
HostTerminalEvent::Input(vec![3]),
|
||||
HostTerminalEvent::Break,
|
||||
HostTerminalEvent::LocalSuspend,
|
||||
HostTerminalEvent::Input(b"input".to_vec()),
|
||||
]);
|
||||
|
|
@ -289,21 +314,16 @@ fn terminal_fixture_routes_resize_signals_suspend_prompt_and_nonzero_exit() {
|
|||
assert_eq!(decision.approved_command_argv, [b"/usr/bin/tool".to_vec()]);
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(lazaret_core::records::SessionTerminalEvent::Resize(_))
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(_))
|
||||
)));
|
||||
for signal in [TerminalSignalKind::Interrupt, TerminalSignalKind::Quit] {
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
||||
) if sent.signal == signal
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Input(input))
|
||||
if input.bytes == [3]
|
||||
)));
|
||||
}
|
||||
assert!(!transport.events.iter().any(|event| matches!(
|
||||
assert!(transport.events.iter().any(|event| matches!(
|
||||
event,
|
||||
InteractiveSessionEvent::Terminal(
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
||||
) if sent.signal == TerminalSignalKind::Suspend
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Break(_))
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +355,34 @@ fn terminal_fixture_restores_raw_mode_for_stale_and_lost_coordinator() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_disconnect_cancels_blocked_transport_and_restores_terminal() {
|
||||
let identity = ClientSessionIdentity::generate::<std::convert::Infallible>().unwrap();
|
||||
let binding = binding(&identity);
|
||||
let mut terminal = PtyTerminal::new([
|
||||
HostTerminalEvent::LocalSuspend,
|
||||
HostTerminalEvent::Terminate,
|
||||
]);
|
||||
let mut transport = FixtureTransport {
|
||||
blocked: true,
|
||||
..FixtureTransport::successful(identity.session_id())
|
||||
};
|
||||
|
||||
assert!(matches!(
|
||||
run_interactive_session(
|
||||
&mut transport,
|
||||
&mut terminal,
|
||||
&identity,
|
||||
&binding,
|
||||
&[b"agent".to_vec()],
|
||||
),
|
||||
Err(ClientError::LostConnection)
|
||||
));
|
||||
assert!(transport.cancelled);
|
||||
assert_eq!(terminal.suspend_count, 1);
|
||||
assert!(terminal.restored());
|
||||
}
|
||||
|
||||
fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
||||
SessionBinding {
|
||||
generation_identity: GenerationIdentity {
|
||||
|
|
@ -354,9 +402,38 @@ fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
|||
fn batch(
|
||||
session_id: SessionId,
|
||||
messages: Vec<InteractiveSessionMessage>,
|
||||
) -> InteractiveSessionBatch {
|
||||
InteractiveSessionBatch {
|
||||
) -> InteractiveSessionBatchV2 {
|
||||
InteractiveSessionBatchV2 {
|
||||
session_id,
|
||||
messages,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_modes() -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: 0,
|
||||
output_modes: 0,
|
||||
control_modes: 0,
|
||||
local_modes: 0,
|
||||
line_discipline: 0,
|
||||
input_speed: 38_400,
|
||||
output_speed: 38_400,
|
||||
interrupt: 3,
|
||||
quit: 28,
|
||||
erase: 127,
|
||||
kill: 21,
|
||||
eof: 4,
|
||||
time: 0,
|
||||
minimum: 1,
|
||||
switch: 0,
|
||||
start: 17,
|
||||
stop: 19,
|
||||
suspend: 26,
|
||||
end_of_line: 0,
|
||||
reprint: 18,
|
||||
discard: 15,
|
||||
word_erase: 23,
|
||||
literal_next: 22,
|
||||
end_of_line_2: 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ use rustls::{ClientConfig, ClientConnection, RootCertStore};
|
|||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1";
|
||||
pub const CONTROL_ALPN_V2: &[u8] = b"lazaret-control/2";
|
||||
pub const CONTROL_SERVER_NAME: &str = "coordinator.lazaret.invalid";
|
||||
pub const CONTROL_SOCKET_NAME: &str = "c.sock";
|
||||
pub const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
||||
|
|
@ -237,6 +238,25 @@ impl ControlClient {
|
|||
pub fn config(
|
||||
publication: &ProtocolPublicationIdentity,
|
||||
client: Option<(&TlsCredential, PrivateKeyDer<'static>)>,
|
||||
) -> Result<ClientConfig, ControlClientError> {
|
||||
Self::config_with_alpn(publication, client, CONTROL_ALPN)
|
||||
}
|
||||
|
||||
/// Creates a TLS client configuration for the interactive v2 protocol.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns malformed trust or credential failures.
|
||||
pub fn config_v2(
|
||||
publication: &ProtocolPublicationIdentity,
|
||||
client: Option<(&TlsCredential, PrivateKeyDer<'static>)>,
|
||||
) -> Result<ClientConfig, ControlClientError> {
|
||||
Self::config_with_alpn(publication, client, CONTROL_ALPN_V2)
|
||||
}
|
||||
|
||||
fn config_with_alpn(
|
||||
publication: &ProtocolPublicationIdentity,
|
||||
client: Option<(&TlsCredential, PrivateKeyDer<'static>)>,
|
||||
alpn: &[u8],
|
||||
) -> Result<ClientConfig, ControlClientError> {
|
||||
let mut roots = RootCertStore::empty();
|
||||
roots.add(CertificateDer::from(
|
||||
|
|
@ -258,7 +278,7 @@ impl ControlClient {
|
|||
} else {
|
||||
builder.with_no_client_auth()
|
||||
};
|
||||
config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
|
||||
config.alpn_protocols = vec![alpn.to_vec()];
|
||||
config.enable_early_data = false;
|
||||
config.resumption = rustls::client::Resumption::disabled();
|
||||
Ok(config)
|
||||
|
|
@ -275,6 +295,33 @@ impl ControlClient {
|
|||
) -> Result<
|
||||
rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>,
|
||||
ControlClientError,
|
||||
> {
|
||||
Self::connect_with_alpn(socket_path, config, publication, CONTROL_ALPN)
|
||||
}
|
||||
|
||||
/// Connects a pinned TLS stream using the interactive v2 ALPN.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns socket, TLS, or certificate-profile failures.
|
||||
pub fn connect_v2(
|
||||
socket_path: &Path,
|
||||
config: ClientConfig,
|
||||
publication: &ProtocolPublicationIdentity,
|
||||
) -> Result<
|
||||
rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>,
|
||||
ControlClientError,
|
||||
> {
|
||||
Self::connect_with_alpn(socket_path, config, publication, CONTROL_ALPN_V2)
|
||||
}
|
||||
|
||||
fn connect_with_alpn(
|
||||
socket_path: &Path,
|
||||
config: ClientConfig,
|
||||
publication: &ProtocolPublicationIdentity,
|
||||
alpn: &[u8],
|
||||
) -> Result<
|
||||
rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>,
|
||||
ControlClientError,
|
||||
> {
|
||||
let socket = std::os::unix::net::UnixStream::connect(socket_path)?;
|
||||
let server_name = ServerName::try_from(CONTROL_SERVER_NAME)
|
||||
|
|
@ -288,7 +335,7 @@ impl ControlClient {
|
|||
return Err(ControlClientError::InvalidCertificateProfile);
|
||||
};
|
||||
if connection.protocol_version() != Some(rustls::ProtocolVersion::TLSv1_3)
|
||||
|| connection.alpn_protocol() != Some(CONTROL_ALPN)
|
||||
|| connection.alpn_protocol() != Some(alpn)
|
||||
|| certificate_digest(certificate) != publication.coordinator_tls_certificate_sha256
|
||||
{
|
||||
return Err(ControlClientError::InvalidCertificateProfile);
|
||||
|
|
|
|||
|
|
@ -230,6 +230,7 @@ pub enum ControlRequest {
|
|||
Register(SessionRegistrationRequest),
|
||||
Execute(NonInteractiveSessionRequest),
|
||||
ExecuteInteractive(super::InteractiveSessionRequest),
|
||||
ExecuteInteractiveV2(super::InteractiveSessionRequestV2),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
|
|
|
|||
|
|
@ -4,25 +4,28 @@ use super::{
|
|||
DefaultAction, DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, Domain,
|
||||
DomainStore, DomainStoreMetadata, EnvironmentReference, EscalationOutcome, Generation,
|
||||
GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity, HelperOwnership,
|
||||
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, Mailbox,
|
||||
MailboxCancellation, MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk,
|
||||
MountIdentity, NamespaceIdentity, NetworkCanaryObservation, NetworkObservation,
|
||||
NetworkOwnership, NetworkReadinessAttestation, NetworkReadinessEvidence,
|
||||
NetworkReadinessIdentity, NonInteractiveOutput, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, Participant, ParticipantOwnership, ParticipantState,
|
||||
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
|
||||
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
|
||||
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
|
||||
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
|
||||
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream,
|
||||
RunscProcessOwnership, SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding,
|
||||
SessionClose, SessionExit, SessionExitOutcome, SessionRegistrationRequest,
|
||||
SessionRegistrationResponse, SessionRequestCapability, SessionTerminal, SessionTerminalEvent,
|
||||
SessionTerminalMessage, SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership,
|
||||
StateAnchorOwnership, StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata,
|
||||
StoreMetadata, TerminalInput, TerminalOutput, TerminalPrompt, TerminalResize, TerminalSignal,
|
||||
TerminalSignalKind, TlsCredential, TlsRole, TrustManifest, TrustedHostConfigurationV1,
|
||||
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveProtocolVersion,
|
||||
InteractiveSessionBatch, InteractiveSessionBatchV2, InteractiveSessionEvent,
|
||||
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequest,
|
||||
InteractiveSessionRequestV2, Mailbox, MailboxCancellation, MailboxRequest, MailboxResult,
|
||||
MailboxResultAck, MailboxResultChunk, MountIdentity, NamespaceIdentity,
|
||||
NetworkCanaryObservation, NetworkObservation, NetworkOwnership, NetworkReadinessAttestation,
|
||||
NetworkReadinessEvidence, NetworkReadinessIdentity, NonInteractiveOutput,
|
||||
NonInteractiveSessionMessage, NonInteractiveSessionRequest, Participant, ParticipantOwnership,
|
||||
ParticipantState, PersistedHostProcessOwnership, PidfsProcessAuthorityV1,
|
||||
PidfsProcessIdentityV1, PolicyAction, PolicyDelivery, PrefixRule, PrepareOperation,
|
||||
PrepareStageState, PreparedArtifact, PreparedLowerOwnership, ProcessOwnershipCommitAck,
|
||||
ProcessOwnershipMessageVersion, ProtocolCredential, ProtocolCredentialRole,
|
||||
ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership, ResolvedAutoApprovalRule,
|
||||
ResolvedVolume, Resolver, ResultStream, RunscProcessOwnership, SandboxCredentialBundle,
|
||||
SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, SessionExitOutcome,
|
||||
SessionRegistrationRequest, SessionRegistrationResponse, SessionRequestCapability,
|
||||
SessionTerminal, SessionTerminalEvent, SessionTerminalEventV2, SessionTerminalMessage,
|
||||
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
|
||||
StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata, StoreMetadata,
|
||||
TerminalBreak, TerminalInput, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
TerminalSignal, TerminalSignalKind, TlsCredential, TlsRole, TrustManifest,
|
||||
TrustedHostConfigurationV1,
|
||||
};
|
||||
use crate::protocol::{BootstrapWaitOutcome, BootstrapWaitRequest};
|
||||
|
||||
|
|
@ -67,16 +70,23 @@ schema_types!(
|
|||
"ProcessOwnershipCommitAck" => ProcessOwnershipCommitAck,
|
||||
"GuestProcessIdentity" => GuestProcessIdentity,
|
||||
"SessionTerminalEvent" => SessionTerminalEvent,
|
||||
"SessionTerminalEventV2" => SessionTerminalEventV2,
|
||||
"TerminalInput" => TerminalInput,
|
||||
"TerminalResize" => TerminalResize,
|
||||
"TerminalModesV2" => TerminalModesV2,
|
||||
"TerminalBreak" => TerminalBreak,
|
||||
"TerminalSignal" => TerminalSignal,
|
||||
"TerminalSignalKind" => TerminalSignalKind,
|
||||
"SessionClose" => SessionClose,
|
||||
"SessionTerminalMessage" => SessionTerminalMessage,
|
||||
"InteractiveSessionRequest" => InteractiveSessionRequest,
|
||||
"InteractiveProtocolVersion" => InteractiveProtocolVersion,
|
||||
"InteractiveSessionRequestV2" => InteractiveSessionRequestV2,
|
||||
"InteractiveSessionEvent" => InteractiveSessionEvent,
|
||||
"InteractiveSessionEventV2" => InteractiveSessionEventV2,
|
||||
"InteractiveSessionMessage" => InteractiveSessionMessage,
|
||||
"InteractiveSessionBatch" => InteractiveSessionBatch,
|
||||
"InteractiveSessionBatchV2" => InteractiveSessionBatchV2,
|
||||
"TerminalPrompt" => TerminalPrompt,
|
||||
"NonInteractiveSessionRequest" => NonInteractiveSessionRequest,
|
||||
"NonInteractiveOutput" => NonInteractiveOutput,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ use crate::identity::{RequestDigest, RequestId};
|
|||
|
||||
use super::{Deserialize, NonZeroU16, ProtocolVersion, Serialize, SessionId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum InteractiveProtocolVersion {
|
||||
V2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalInput {
|
||||
pub session_id: SessionId,
|
||||
|
|
@ -17,6 +22,34 @@ pub struct TerminalResize {
|
|||
pub pixel_height: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalModesV2 {
|
||||
pub input_modes: u32,
|
||||
pub output_modes: u32,
|
||||
pub control_modes: u32,
|
||||
pub local_modes: u32,
|
||||
pub line_discipline: u8,
|
||||
pub input_speed: u32,
|
||||
pub output_speed: u32,
|
||||
pub interrupt: u8,
|
||||
pub quit: u8,
|
||||
pub erase: u8,
|
||||
pub kill: u8,
|
||||
pub eof: u8,
|
||||
pub time: u8,
|
||||
pub minimum: u8,
|
||||
pub switch: u8,
|
||||
pub start: u8,
|
||||
pub stop: u8,
|
||||
pub suspend: u8,
|
||||
pub end_of_line: u8,
|
||||
pub reprint: u8,
|
||||
pub discard: u8,
|
||||
pub word_erase: u8,
|
||||
pub literal_next: u8,
|
||||
pub end_of_line_2: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum TerminalSignalKind {
|
||||
Interrupt,
|
||||
|
|
@ -35,6 +68,11 @@ pub struct SessionClose {
|
|||
pub session_id: SessionId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalBreak {
|
||||
pub session_id: SessionId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SessionTerminalEvent {
|
||||
Input(TerminalInput),
|
||||
|
|
@ -43,6 +81,14 @@ pub enum SessionTerminalEvent {
|
|||
Close(SessionClose),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum SessionTerminalEventV2 {
|
||||
Input(TerminalInput),
|
||||
Resize(TerminalResize),
|
||||
Break(TerminalBreak),
|
||||
Close(SessionClose),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalOutput {
|
||||
pub session_id: SessionId,
|
||||
|
|
@ -147,6 +193,15 @@ pub struct InteractiveSessionRequest {
|
|||
pub initial_size: TerminalResize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct InteractiveSessionRequestV2 {
|
||||
pub protocol_version: InteractiveProtocolVersion,
|
||||
pub session_id: SessionId,
|
||||
pub argv: Vec<Vec<u8>>,
|
||||
pub initial_size: TerminalResize,
|
||||
pub initial_modes: TerminalModesV2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct TerminalPrompt {
|
||||
pub session_id: SessionId,
|
||||
|
|
@ -169,6 +224,18 @@ pub enum InteractiveSessionEvent {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum InteractiveSessionEventV2 {
|
||||
Terminal(SessionTerminalEventV2),
|
||||
PromptDecision {
|
||||
session_id: SessionId,
|
||||
signed_decision: Vec<u8>,
|
||||
},
|
||||
Poll {
|
||||
session_id: SessionId,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum InteractiveSessionMessage {
|
||||
Terminal(SessionTerminalMessage),
|
||||
|
|
@ -181,6 +248,12 @@ pub struct InteractiveSessionBatch {
|
|||
pub messages: Vec<InteractiveSessionMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct InteractiveSessionBatchV2 {
|
||||
pub session_id: SessionId,
|
||||
pub messages: Vec<InteractiveSessionMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub struct NonInteractiveSessionRequest {
|
||||
pub protocol_version: super::ProtocolVersion,
|
||||
|
|
|
|||
|
|
@ -288,6 +288,35 @@ fn session_id() -> SessionId {
|
|||
SessionId::new(opaque(5))
|
||||
}
|
||||
|
||||
fn terminal_modes_v2() -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: 1,
|
||||
output_modes: 2,
|
||||
control_modes: 3,
|
||||
local_modes: 4,
|
||||
line_discipline: 0,
|
||||
input_speed: 38_400,
|
||||
output_speed: 38_400,
|
||||
interrupt: 3,
|
||||
quit: 28,
|
||||
erase: 127,
|
||||
kill: 21,
|
||||
eof: 4,
|
||||
time: 0,
|
||||
minimum: 1,
|
||||
switch: 0,
|
||||
start: 17,
|
||||
stop: 19,
|
||||
suspend: 26,
|
||||
end_of_line: 0,
|
||||
reprint: 18,
|
||||
discard: 15,
|
||||
word_erase: 23,
|
||||
literal_next: 22,
|
||||
end_of_line_2: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn process(seed: u32) -> PidfsProcessIdentityV1 {
|
||||
PidfsProcessIdentityV1::new(
|
||||
PidfsProcessAuthorityV1 {
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
}),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
SessionTerminalEventV2::Break(TerminalBreak {
|
||||
session_id: session_id(),
|
||||
}),
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalInput {
|
||||
session_id: session_id(),
|
||||
|
|
@ -162,6 +168,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(terminal_modes_v2(), &mut seen);
|
||||
verify_shape(
|
||||
TerminalBreak {
|
||||
session_id: session_id(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
TerminalSignal {
|
||||
session_id: session_id(),
|
||||
|
|
@ -227,12 +240,29 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(InteractiveProtocolVersion::V2, &mut seen);
|
||||
verify_shape(
|
||||
InteractiveSessionRequestV2 {
|
||||
protocol_version: InteractiveProtocolVersion::V2,
|
||||
session_id: session_id(),
|
||||
argv: vec![b"tool".to_vec()],
|
||||
initial_size: interactive_resize,
|
||||
initial_modes: terminal_modes_v2(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
InteractiveSessionEvent::Poll {
|
||||
session_id: session_id(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
InteractiveSessionEventV2::Poll {
|
||||
session_id: session_id(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
let prompt = TerminalPrompt {
|
||||
session_id: session_id(),
|
||||
request_id: RequestId::new(opaque(54)),
|
||||
|
|
@ -251,6 +281,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
|||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(
|
||||
InteractiveSessionBatchV2 {
|
||||
session_id: session_id(),
|
||||
messages: Vec::new(),
|
||||
},
|
||||
&mut seen,
|
||||
);
|
||||
verify_shape(generation_identity(), &mut seen);
|
||||
verify_shape(trust_manifest(), &mut seen);
|
||||
verify_shape(tls_credential.clone(), &mut seen);
|
||||
|
|
@ -649,8 +686,8 @@ persistent_dirs = []
|
|||
assert_eq!(
|
||||
seen.digest(),
|
||||
[
|
||||
54, 187, 13, 149, 227, 157, 1, 54, 234, 66, 127, 9, 34, 65, 173, 20, 214, 200, 126, 67,
|
||||
1, 216, 152, 76, 167, 32, 177, 218, 0, 192, 175, 28,
|
||||
201, 9, 82, 185, 208, 17, 32, 181, 237, 9, 161, 239, 26, 109, 45, 148, 192, 138, 104,
|
||||
114, 203, 147, 61, 180, 162, 184, 139, 77, 227, 8, 27, 128,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,7 @@ fn control_corpus(digest: &mut Sha256) {
|
|||
ControlRequest::Register(_) => ControlRequest::Register(registration_request()),
|
||||
ControlRequest::Execute(_) => ControlRequest::Execute(NonInteractiveSessionRequest { protocol_version: ProtocolVersion::V1, session_id: session_id(), argv: vec![b"tool".to_vec()] }),
|
||||
ControlRequest::ExecuteInteractive(_) => ControlRequest::ExecuteInteractive(InteractiveSessionRequest { protocol_version: ProtocolVersion::V1, session_id: session_id(), argv: vec![b"tool".to_vec()], initial_size: resize }),
|
||||
ControlRequest::ExecuteInteractiveV2(_) => ControlRequest::ExecuteInteractiveV2(InteractiveSessionRequestV2 { protocol_version: InteractiveProtocolVersion::V2, session_id: session_id(), argv: vec![b"tool".to_vec()], initial_size: resize, initial_modes: terminal_modes_v2() }),
|
||||
});
|
||||
enum_corpus!(digest, ControlResponse, {
|
||||
ControlResponse::Enrolled(_) => ControlResponse::Enrolled(enrollment_response()),
|
||||
|
|
@ -231,6 +232,9 @@ fn credential_corpus(digest: &mut Sha256) {
|
|||
enum_corpus!(digest, ProtocolVersion, {
|
||||
ProtocolVersion::V1 => ProtocolVersion::V1,
|
||||
});
|
||||
enum_corpus!(digest, InteractiveProtocolVersion, {
|
||||
InteractiveProtocolVersion::V2 => InteractiveProtocolVersion::V2,
|
||||
});
|
||||
enum_corpus!(digest, TlsRole, {
|
||||
TlsRole::SessionCoordinator => TlsRole::SessionCoordinator,
|
||||
TlsRole::ClientCli => TlsRole::ClientCli,
|
||||
|
|
@ -325,8 +329,8 @@ fn every_serialized_enum_variant_has_fixed_shape_evidence() {
|
|||
assert_eq!(
|
||||
digest.finalize().as_slice(),
|
||||
[
|
||||
63, 126, 242, 211, 164, 165, 11, 185, 180, 135, 109, 135, 92, 142, 111, 169, 142, 215,
|
||||
49, 4, 170, 169, 119, 237, 235, 161, 192, 89, 92, 93, 88, 35,
|
||||
16, 136, 82, 10, 228, 231, 132, 63, 189, 59, 106, 16, 164, 103, 232, 11, 245, 31, 144,
|
||||
53, 91, 201, 91, 74, 66, 14, 174, 78, 110, 17, 181, 21,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -553,6 +553,10 @@ impl<'a> CoordinatorLifecycleAdapter<'a> {
|
|||
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
|
||||
.stop_sandbox(&sandbox)
|
||||
.map_err(failure)?;
|
||||
self.plan
|
||||
.interactive_executor
|
||||
.clean_after_sandbox_absence()
|
||||
.map_err(failure)?;
|
||||
if self.plan.git_routing.is_some() {
|
||||
let git_routing = GitRoutingCoordinator::new(&store);
|
||||
self.runtime
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
pub use lazaret_control_client::{CONTROL_ALPN, CONTROL_SERVER_NAME, PendingClientEnrollment};
|
||||
pub use lazaret_control_client::{
|
||||
CONTROL_ALPN, CONTROL_ALPN_V2, CONTROL_SERVER_NAME, PendingClientEnrollment,
|
||||
};
|
||||
use lazaret_control_client::{ControlClient, ControlClientError};
|
||||
use lazaret_core::codec::{CodecError, decode, encode};
|
||||
use lazaret_core::configuration::{ConfinedPathPlan, EnvironmentValue};
|
||||
|
|
@ -24,13 +26,13 @@ use lazaret_core::protocol::{
|
|||
};
|
||||
use lazaret_core::records::{
|
||||
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
|
||||
DurableDecision, GenerationIdentity, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionMessage, InteractiveSessionRequest, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, ProtocolCredential, ProtocolCredentialRole,
|
||||
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, PublicationOwnership,
|
||||
SandboxCredentialBundle, Session, SessionBinding, SessionExit, SessionPhase,
|
||||
SessionRegistrationRequest, SessionRegistrationResponse, TlsCredential, TlsRole, TlsSubjectId,
|
||||
TrustManifest,
|
||||
DurableDecision, GenerationIdentity, InteractiveProtocolVersion, InteractiveSessionBatch,
|
||||
InteractiveSessionBatchV2, InteractiveSessionEventV2, InteractiveSessionMessage,
|
||||
InteractiveSessionRequestV2, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, ProtocolSubjectId,
|
||||
ProtocolVersion, PublicationOwnership, SandboxCredentialBundle, Session, SessionBinding,
|
||||
SessionExit, SessionPhase, SessionRegistrationRequest, SessionRegistrationResponse,
|
||||
SessionTerminalMessage, TerminalOutput, TlsCredential, TlsRole, TlsSubjectId, TrustManifest,
|
||||
};
|
||||
use rcgen::{
|
||||
BasicConstraints, CertificateParams, CertificateSigningRequestParams, CertifiedIssuer,
|
||||
|
|
@ -354,7 +356,7 @@ impl TrustAnchor {
|
|||
)],
|
||||
PrivateKeyDer::Pkcs8(self.coordinator_tls_private_key.clone_key()),
|
||||
)?;
|
||||
config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
|
||||
config.alpn_protocols = vec![CONTROL_ALPN_V2.to_vec(), CONTROL_ALPN.to_vec()];
|
||||
config.max_early_data_size = 0;
|
||||
config.send_tls13_tickets = 0;
|
||||
config.session_storage = Arc::new(NoServerSessionStorage {});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use super::{
|
||||
Arc, AsFd, BTreeMap, CONTROL_SOCKET_NAME, ContainerId, ControlError, EnvironmentValue, File,
|
||||
FileIdentity, InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest,
|
||||
MetadataExt, Mode, NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags,
|
||||
OpenOptions, OsStr, OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf,
|
||||
ProcessExit, ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
|
||||
STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig, SessionExit, SessionId, SessionRegistry,
|
||||
TargetCommand, TrustAnchor, Write, child_broker, fchmod, fmt, io, openat,
|
||||
remove_if_identity_matches,
|
||||
FileIdentity, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||
InteractiveSessionRequestV2, MetadataExt, Mode, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, OFlags, OpenOptions, OsStr, OsString, OsStringExt,
|
||||
PUBLICATION_IDENTITY_NAME, Path, PathBuf, ProcessExit, ProtocolPublicationIdentity, Read,
|
||||
RunscCommand, STAGED_CONTROL_SOCKET_NAME, STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig,
|
||||
SessionExit, SessionId, SessionRegistry, TargetCommand, TrustAnchor, Write, child_broker,
|
||||
fchmod, fmt, io, openat, remove_if_identity_matches,
|
||||
};
|
||||
|
||||
pub(super) struct ControlPaths {
|
||||
|
|
@ -194,8 +194,8 @@ pub trait InteractiveControlSession: Send {
|
|||
/// Returns an exact-session terminal, prompt-routing, or transport failure.
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ControlError>;
|
||||
event: InteractiveSessionEventV2,
|
||||
) -> Result<InteractiveSessionBatchV2, ControlError>;
|
||||
}
|
||||
|
||||
pub trait InteractiveSessionExecutor: Send + Sync {
|
||||
|
|
@ -203,8 +203,12 @@ pub trait InteractiveSessionExecutor: Send + Sync {
|
|||
/// Returns an acquisition or exact-session identity failure.
|
||||
fn start(
|
||||
&self,
|
||||
request: &InteractiveSessionRequest,
|
||||
request: &InteractiveSessionRequestV2,
|
||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError>;
|
||||
|
||||
/// # Errors
|
||||
/// Refuses cleanup unless every terminal resource is absent after sandbox teardown.
|
||||
fn clean_after_sandbox_absence(&self) -> Result<(), ControlError>;
|
||||
}
|
||||
|
||||
pub(super) struct InteractiveExecutionUnavailable;
|
||||
|
|
@ -212,10 +216,14 @@ pub(super) struct InteractiveExecutionUnavailable;
|
|||
impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
|
||||
fn start(
|
||||
&self,
|
||||
_request: &InteractiveSessionRequest,
|
||||
_request: &InteractiveSessionRequestV2,
|
||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
|
||||
fn clean_after_sandbox_absence(&self) -> Result<(), ControlError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunscNonInteractiveExecutor {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,20 @@
|
|||
use super::{
|
||||
AcceptedControlConnection, Arc, AsFd, AsRawFd, CONTROL_ALPN, CONTROL_SOCKET_NAME,
|
||||
CertificateDigest, CertificateParams, ClientConfig, ClientConnection, ClientEnrollmentResponse,
|
||||
ClientId, ConfinedPathPlan, ControlClient, ControlError, ControlPaths, ControlRequest,
|
||||
ControlResponse, ControlSessionAdmission, Digest, DistinguishedName, DnType, Duration,
|
||||
ExecutionUnavailable, ExtendedKeyUsagePurpose, File, FileExt, FileIdentity, FileTypeExt,
|
||||
GenerationIdentity, HostCoordinatorDomainStore, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionExecutor, InteractiveSessionMessage, IsCa,
|
||||
AcceptedControlConnection, Arc, AsFd, AsRawFd, CONTROL_ALPN, CONTROL_ALPN_V2,
|
||||
CONTROL_SOCKET_NAME, CertificateDigest, CertificateParams, ClientConfig, ClientConnection,
|
||||
ClientEnrollmentResponse, ClientId, ConfinedPathPlan, ControlClient, ControlError,
|
||||
ControlPaths, ControlRequest, ControlResponse, ControlSessionAdmission, Digest,
|
||||
DistinguishedName, DnType, Duration, ExecutionUnavailable, ExtendedKeyUsagePurpose, File,
|
||||
FileExt, FileIdentity, FileTypeExt, GenerationIdentity, HostCoordinatorDomainStore,
|
||||
InteractiveProtocolVersion, InteractiveSessionBatch, InteractiveSessionBatchV2,
|
||||
InteractiveSessionEventV2, InteractiveSessionExecutor, InteractiveSessionMessage, IsCa,
|
||||
KeyUsagePurpose, MAX_CONTROL_BYTES, MAX_ENROLLMENT_BYTES, MetadataExt, Mode,
|
||||
NonInteractiveSessionExecutor, NonInteractiveSessionMessage, OFlags, OffsetDateTime,
|
||||
PUBLICATION_IDENTITY_NAME, Path, PathBuf, PrivateKeyDer, ProtocolPublicationIdentity,
|
||||
ProtocolVersion, PublicationOwnership, PublishedControlFace, Read, ServerConnection,
|
||||
SessionBinding, SessionId, SessionPhase, SessionRegistrationResponse, SessionRegistry, Sha256,
|
||||
SigningKey, StagedControlFace, TlsCredential, TrustAnchor, Write, decode, encode, fmt, io,
|
||||
issue_session_capability, openat, publication_recovery_ownership, socket_peercred,
|
||||
SessionBinding, SessionExit, SessionId, SessionPhase, SessionRegistrationResponse,
|
||||
SessionRegistry, SessionTerminalMessage, Sha256, SigningKey, StagedControlFace, TerminalOutput,
|
||||
TlsCredential, TrustAnchor, Write, decode, encode, fmt, io, issue_session_capability, openat,
|
||||
publication_recovery_ownership, socket_peercred,
|
||||
};
|
||||
|
||||
impl PublishedControlFace {
|
||||
|
|
@ -317,9 +319,11 @@ impl PublishedControlFace {
|
|||
while connection.is_handshaking() {
|
||||
connection.complete_io(&mut socket)?;
|
||||
}
|
||||
if connection.alpn_protocol() != Some(CONTROL_ALPN) {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let interactive_v2 = match connection.alpn_protocol() {
|
||||
Some(protocol) if protocol == CONTROL_ALPN_V2 => true,
|
||||
Some(protocol) if protocol == CONTROL_ALPN => false,
|
||||
_ => return Err(ControlError::InvalidRequest),
|
||||
};
|
||||
let peer_certificate = connection.peer_certificates().map(|certificates| {
|
||||
certificates
|
||||
.iter()
|
||||
|
|
@ -332,6 +336,7 @@ impl PublishedControlFace {
|
|||
Some(certificates) => self.serve_registration(
|
||||
&mut stream,
|
||||
&certificates,
|
||||
interactive_v2,
|
||||
&mut admit,
|
||||
&mut registered,
|
||||
&mut disconnect,
|
||||
|
|
@ -362,6 +367,7 @@ impl PublishedControlFace {
|
|||
&self,
|
||||
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
|
||||
certificates: &[Vec<u8>],
|
||||
interactive_v2: bool,
|
||||
admit: &mut Admit,
|
||||
registered: &mut Registered,
|
||||
disconnect: &mut Disconnect,
|
||||
|
|
@ -410,8 +416,21 @@ impl PublishedControlFace {
|
|||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
self.serve_execution(stream, request, session_id, interactive_v2)
|
||||
})();
|
||||
let disconnected = disconnect(session_id);
|
||||
result.and(disconnected)
|
||||
}
|
||||
|
||||
fn serve_execution(
|
||||
&self,
|
||||
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
|
||||
request: ControlRequest,
|
||||
session_id: SessionId,
|
||||
interactive_v2: bool,
|
||||
) -> Result<(), ControlError> {
|
||||
match request {
|
||||
ControlRequest::Execute(request) => {
|
||||
ControlRequest::Execute(request) if !interactive_v2 => {
|
||||
validate_command(request.session_id, session_id, &request.argv)?;
|
||||
let mut send_message = |message: NonInteractiveSessionMessage| {
|
||||
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
||||
|
|
@ -424,22 +443,46 @@ impl PublishedControlFace {
|
|||
require_session_id(outcome.session_id(), request.session_id)?;
|
||||
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
||||
}
|
||||
ControlRequest::ExecuteInteractive(request) => {
|
||||
ControlRequest::ExecuteInteractive(request) if !interactive_v2 => {
|
||||
validate_command(request.session_id, session_id, &request.argv)?;
|
||||
if request.initial_size.session_id != session_id {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
write_frame(
|
||||
stream,
|
||||
&InteractiveSessionBatch {
|
||||
session_id,
|
||||
messages: vec![
|
||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Output(
|
||||
TerminalOutput {
|
||||
session_id,
|
||||
bytes: b"Lazaret interactive protocol upgrade required\r\n"
|
||||
.to_vec(),
|
||||
},
|
||||
)),
|
||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(
|
||||
SessionExit::runtime_lost(session_id),
|
||||
)),
|
||||
],
|
||||
},
|
||||
)
|
||||
}
|
||||
ControlRequest::ExecuteInteractiveV2(request) if interactive_v2 => {
|
||||
validate_command(request.session_id, session_id, &request.argv)?;
|
||||
if request.initial_size.session_id != session_id
|
||||
|| request.protocol_version != InteractiveProtocolVersion::V2
|
||||
{
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let mut terminal = self.interactive_executor.start(&request)?;
|
||||
let mut event = InteractiveSessionEvent::Poll { session_id };
|
||||
let mut event = InteractiveSessionEventV2::Poll { session_id };
|
||||
loop {
|
||||
let batch = terminal.exchange(event)?;
|
||||
let batch: InteractiveSessionBatchV2 = terminal.exchange(event)?;
|
||||
require_interactive_batch(&batch, session_id)?;
|
||||
let complete = batch.messages.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
InteractiveSessionMessage::Terminal(
|
||||
lazaret_core::records::SessionTerminalMessage::Exit(_)
|
||||
)
|
||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(_))
|
||||
)
|
||||
});
|
||||
write_frame(stream, &batch)?;
|
||||
|
|
@ -450,14 +493,13 @@ impl PublishedControlFace {
|
|||
require_interactive_event(&event, session_id)?;
|
||||
}
|
||||
}
|
||||
ControlRequest::Enroll(_) | ControlRequest::Register(_) => {
|
||||
Err(ControlError::InvalidRequest)
|
||||
ControlRequest::Execute(_)
|
||||
| ControlRequest::ExecuteInteractive(_)
|
||||
| ControlRequest::ExecuteInteractiveV2(_)
|
||||
| ControlRequest::Enroll(_)
|
||||
| ControlRequest::Register(_) => Err(ControlError::InvalidRequest),
|
||||
}
|
||||
}
|
||||
})();
|
||||
let disconnected = disconnect(session_id);
|
||||
result.and(disconnected)
|
||||
}
|
||||
|
||||
fn validate_publication(&self) -> Result<(), ControlError> {
|
||||
self.paths.validate_root(self.uid)?;
|
||||
|
|
@ -613,24 +655,24 @@ fn require_session_id(actual: SessionId, expected: SessionId) -> Result<(), Cont
|
|||
}
|
||||
|
||||
fn require_interactive_event(
|
||||
event: &InteractiveSessionEvent,
|
||||
event: &InteractiveSessionEventV2,
|
||||
expected: SessionId,
|
||||
) -> Result<(), ControlError> {
|
||||
let actual = match event {
|
||||
InteractiveSessionEvent::Terminal(event) => match event {
|
||||
lazaret_core::records::SessionTerminalEvent::Input(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Resize(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Close(event) => event.session_id,
|
||||
InteractiveSessionEventV2::Terminal(event) => match event {
|
||||
lazaret_core::records::SessionTerminalEventV2::Input(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEventV2::Resize(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEventV2::Break(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEventV2::Close(event) => event.session_id,
|
||||
},
|
||||
InteractiveSessionEvent::PromptDecision { session_id, .. }
|
||||
| InteractiveSessionEvent::Poll { session_id } => *session_id,
|
||||
InteractiveSessionEventV2::PromptDecision { session_id, .. }
|
||||
| InteractiveSessionEventV2::Poll { session_id } => *session_id,
|
||||
};
|
||||
require_session_id(actual, expected)
|
||||
}
|
||||
|
||||
fn require_interactive_batch(
|
||||
batch: &InteractiveSessionBatch,
|
||||
batch: &InteractiveSessionBatchV2,
|
||||
expected: SessionId,
|
||||
) -> Result<(), ControlError> {
|
||||
require_session_id(batch.session_id, expected)?;
|
||||
|
|
|
|||
|
|
@ -167,6 +167,31 @@ fn rustls_refuses_client_leaf_with_server_role() {
|
|||
assert!(server.is_err() || client.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_client_fails_the_tls_handshake_with_a_v1_only_server() {
|
||||
let anchor = TrustAnchor::create(generation()).unwrap();
|
||||
let key = KeyPair::generate_for(&PKCS_ED25519).unwrap();
|
||||
let certificate = leaf_parameters(
|
||||
"client.lazaret.invalid",
|
||||
ExtendedKeyUsagePurpose::ClientAuth,
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.unwrap()
|
||||
.signed_by(&key, &anchor.tls_ca)
|
||||
.unwrap();
|
||||
let mut client_config = client_config_with_certificate(
|
||||
&anchor,
|
||||
certificate.der().clone(),
|
||||
PrivatePkcs8KeyDer::from(key.serialize_der()),
|
||||
);
|
||||
client_config.alpn_protocols = vec![CONTROL_ALPN_V2.to_vec()];
|
||||
let mut server_config = anchor.server_config().unwrap();
|
||||
server_config.alpn_protocols = vec![CONTROL_ALPN.to_vec()];
|
||||
|
||||
let (server, client) = handshake_pair(server_config, client_config);
|
||||
assert!(server.is_err() || client.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustls_refuses_server_leaf_with_client_role() {
|
||||
let anchor = TrustAnchor::create(generation()).unwrap();
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@ use lazaret_core::identity::{
|
|||
};
|
||||
use lazaret_core::protocol::VerifiedSession;
|
||||
use lazaret_core::records::{
|
||||
DeveloperDecision, DomainStore, DurableDecision, GuestProcessIdentity, InteractiveSessionBatch,
|
||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest,
|
||||
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
|
||||
ProcessOwnershipCommitAck, ProtocolVersion, PtyIdentity, SessionExit, SessionPhase,
|
||||
SessionTerminal, SessionTerminalEvent, SessionTerminalFields, SessionTerminalMessage,
|
||||
SessionTerminalState, StagedProcessOwnership, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
TerminalSignalKind,
|
||||
DeveloperDecision, DomainStore, DurableDecision, GuestProcessIdentity,
|
||||
InteractiveProtocolVersion, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||
InteractiveSessionMessage, InteractiveSessionRequestV2, ParticipantOwnership, ParticipantRole,
|
||||
ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck, PtyIdentity,
|
||||
SessionExit, SessionPhase, SessionTerminal, SessionTerminalEventV2, SessionTerminalFields,
|
||||
SessionTerminalMessage, SessionTerminalState, StagedProcessOwnership, TerminalModesV2,
|
||||
TerminalOutput, TerminalPrompt, TerminalResize,
|
||||
};
|
||||
use lazaret_linux::process::{
|
||||
ChildStandardIo, ProcessExit, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
||||
|
|
@ -29,7 +29,10 @@ use lazaret_linux::process::{
|
|||
use lazaret_sandbox_contract::SANDBOX_INIT_DESTINATION;
|
||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat};
|
||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
||||
use rustix::termios::{Winsize, tcsetwinsize};
|
||||
use rustix::termios::{
|
||||
ControlModes, InputModes, LocalModes, OptionalActions, OutputModes, SpecialCodeIndex, Winsize,
|
||||
tcgetattr, tcsendbreak, tcsetattr, tcsetwinsize,
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::control::{ControlError, InteractiveControlSession, InteractiveSessionExecutor};
|
||||
|
|
@ -440,6 +443,7 @@ impl PromptRouteCancellation for SessionPromptTransport {
|
|||
pub trait SessionTerminalProcess: Send {
|
||||
fn write_input(&mut self, bytes: &[u8]) -> io::Result<usize>;
|
||||
fn resize(&self, resize: TerminalResize) -> io::Result<()>;
|
||||
fn send_break(&self) -> io::Result<()>;
|
||||
fn read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize>;
|
||||
fn exec_exited(&self) -> io::Result<bool>;
|
||||
fn signal_group(&self, signal: &str) -> Result<(), TerminalError>;
|
||||
|
|
@ -471,6 +475,8 @@ pub struct TerminalAcquisition {
|
|||
pub environment: BTreeMap<String, EnvironmentValue>,
|
||||
pub internal_pid_file: PathBuf,
|
||||
pub argv: Vec<Vec<u8>>,
|
||||
pub initial_size: TerminalResize,
|
||||
pub initial_modes: TerminalModesV2,
|
||||
}
|
||||
|
||||
pub struct HostCoordinatorTerminalRecorder {
|
||||
|
|
|
|||
|
|
@ -3,12 +3,41 @@ use super::{
|
|||
HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder, Instant, MAX_DRAIN_BYTES,
|
||||
MAX_EVENT_BYTES, MAX_INPUT_QUEUE_BYTES, MAX_OUTPUT_BYTES, POLL_INTERVAL,
|
||||
PersistedHostProcessOwnership, ProcessExit, PromptRouteCancellation, SIGNAL_GRACE, SessionExit,
|
||||
SessionId, SessionTerminalEngine, SessionTerminalEvent, SessionTerminalFactory,
|
||||
SessionId, SessionTerminalEngine, SessionTerminalEventV2, SessionTerminalFactory,
|
||||
SessionTerminalMessage, SessionTerminalProcess, SessionTerminalState, TerminalAcquisition,
|
||||
TerminalError, TerminalLoss, TerminalOutput, TerminalRoute, TerminalSignalKind, VecDeque,
|
||||
clean_terminal, copy_terminal_state, io, terminal_snapshot,
|
||||
TerminalError, TerminalLoss, TerminalOutput, TerminalRoute, VecDeque, clean_terminal,
|
||||
copy_terminal_state, io, terminal_snapshot,
|
||||
};
|
||||
|
||||
fn require_exec_absent<Open>(
|
||||
ownership: &PersistedHostProcessOwnership,
|
||||
open: &mut Open,
|
||||
) -> Result<(), TerminalError>
|
||||
where
|
||||
Open: FnMut(
|
||||
super::PidfsProcessIdentityV1,
|
||||
) -> io::Result<lazaret_linux::process::PidfsRecoveryMatch>,
|
||||
{
|
||||
let Ok(identity) = ownership.authoritative_pidfs() else {
|
||||
return Ok(());
|
||||
};
|
||||
match open(*identity) {
|
||||
Ok(lazaret_linux::process::PidfsRecoveryMatch::BootMismatch) => Ok(()),
|
||||
Ok(lazaret_linux::process::PidfsRecoveryMatch::Matched(process))
|
||||
if process.has_exited()? =>
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
Ok(lazaret_linux::process::PidfsRecoveryMatch::Matched(_)) => {
|
||||
Err(TerminalError::AuthorityMismatch)
|
||||
}
|
||||
Err(error) if error.raw_os_error() == Some(rustix::io::Errno::SRCH.raw_os_error()) => {
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => Err(error.into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionTerminalEngine {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
|
|
@ -58,6 +87,56 @@ impl SessionTerminalEngine {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn clean_after_sandbox_absence<Open>(
|
||||
&self,
|
||||
generation_id: GenerationId,
|
||||
container_id: super::ContainerId,
|
||||
host_coordinator: super::PidfsProcessIdentityV1,
|
||||
mut open: Open,
|
||||
) -> Result<(), TerminalError>
|
||||
where
|
||||
Open: FnMut(
|
||||
super::PidfsProcessIdentityV1,
|
||||
) -> io::Result<lazaret_linux::process::PidfsRecoveryMatch>,
|
||||
{
|
||||
if self
|
||||
.active
|
||||
.values()
|
||||
.any(|terminal| terminal.generation_id == generation_id)
|
||||
{
|
||||
return Err(TerminalError::AuthorityMismatch);
|
||||
}
|
||||
let snapshot = self.recorder.store.snapshot()?;
|
||||
for terminal in snapshot
|
||||
.session_terminals
|
||||
.iter()
|
||||
.filter(|terminal| terminal.generation_id() == generation_id)
|
||||
{
|
||||
if terminal.state() == SessionTerminalState::Clean {
|
||||
continue;
|
||||
}
|
||||
let closed = snapshot.sessions.iter().any(|session| {
|
||||
session.generation_id == generation_id
|
||||
&& session.session_id == terminal.session_id()
|
||||
&& matches!(
|
||||
session.session_phase,
|
||||
super::SessionPhase::Closed | super::SessionPhase::Failed
|
||||
)
|
||||
});
|
||||
if terminal.state() != SessionTerminalState::ReleaseIntent
|
||||
|| terminal.container_id() != Some(container_id)
|
||||
|| terminal.host_coordinator().authoritative_pidfs().ok() != Some(&host_coordinator)
|
||||
|| !closed
|
||||
{
|
||||
return Err(TerminalError::AuthorityMismatch);
|
||||
}
|
||||
require_exec_absent(terminal.exec_client(), &mut open)?;
|
||||
self.recorder
|
||||
.clean(clean_terminal(generation_id, terminal.session_id())?)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
/// Refuses stale, unknown, detached, or releasing Session Terminals.
|
||||
pub fn set_route(
|
||||
|
|
@ -83,15 +162,15 @@ impl SessionTerminalEngine {
|
|||
pub fn handle(
|
||||
&mut self,
|
||||
generation_id: GenerationId,
|
||||
event: SessionTerminalEvent,
|
||||
event: SessionTerminalEventV2,
|
||||
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
||||
let session_id = match &event {
|
||||
SessionTerminalEvent::Input(event) => event.session_id,
|
||||
SessionTerminalEvent::Resize(event) => event.session_id,
|
||||
SessionTerminalEvent::Signal(event) => event.session_id,
|
||||
SessionTerminalEvent::Close(event) => event.session_id,
|
||||
SessionTerminalEventV2::Input(event) => event.session_id,
|
||||
SessionTerminalEventV2::Resize(event) => event.session_id,
|
||||
SessionTerminalEventV2::Break(event) => event.session_id,
|
||||
SessionTerminalEventV2::Close(event) => event.session_id,
|
||||
};
|
||||
if matches!(event, SessionTerminalEvent::Close(_)) {
|
||||
if matches!(event, SessionTerminalEventV2::Close(_)) {
|
||||
return self.close(
|
||||
session_id,
|
||||
generation_id,
|
||||
|
|
@ -112,7 +191,7 @@ impl SessionTerminalEngine {
|
|||
return Err(TerminalError::InactiveRoute);
|
||||
}
|
||||
match event {
|
||||
SessionTerminalEvent::Input(input) => {
|
||||
SessionTerminalEventV2::Input(input) => {
|
||||
if input.bytes.len() > MAX_EVENT_BYTES {
|
||||
return Err(TerminalError::OversizedInput);
|
||||
}
|
||||
|
|
@ -123,20 +202,15 @@ impl SessionTerminalEngine {
|
|||
flush_input(terminal)?;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
SessionTerminalEvent::Resize(resize) => {
|
||||
SessionTerminalEventV2::Resize(resize) => {
|
||||
terminal.process.resize(resize)?;
|
||||
terminal.process.signal_group("WINCH")?;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
SessionTerminalEvent::Signal(signal) => {
|
||||
terminal.process.signal_group(match signal.signal {
|
||||
TerminalSignalKind::Interrupt => "INT",
|
||||
TerminalSignalKind::Quit => "QUIT",
|
||||
TerminalSignalKind::Suspend => "TSTP",
|
||||
})?;
|
||||
SessionTerminalEventV2::Break(_) => {
|
||||
terminal.process.send_break()?;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
SessionTerminalEvent::Close(_) => unreachable!(),
|
||||
SessionTerminalEventV2::Close(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
use super::{
|
||||
Arc, BTreeMap, ContainerId, ControlError, CoordinatorId, DomainId, EnvironmentValue,
|
||||
GenerationId, HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder,
|
||||
InteractiveControlSession, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionExecutor, InteractiveSessionMessage, InteractiveSessionRequest, Mutex,
|
||||
OsString, PathBuf, PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion,
|
||||
RunscSessionTerminalFactory, SessionId, SessionPromptTransport, SessionTerminal,
|
||||
SessionTerminalEvent, SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess,
|
||||
TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute, VecDeque,
|
||||
InteractiveControlSession, InteractiveProtocolVersion, InteractiveSessionBatchV2,
|
||||
InteractiveSessionEventV2, InteractiveSessionExecutor, InteractiveSessionMessage,
|
||||
InteractiveSessionRequestV2, Mutex, OsString, PathBuf, PidfsProcessIdentityV1,
|
||||
PromptRouteCancellation, RunscSessionTerminalFactory, SessionId, SessionPromptTransport,
|
||||
SessionTerminal, SessionTerminalEventV2, SessionTerminalFactory, SessionTerminalMessage,
|
||||
SessionTerminalProcess, TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute,
|
||||
VecDeque,
|
||||
};
|
||||
|
||||
pub(super) struct ActiveTerminal {
|
||||
|
|
@ -91,9 +92,9 @@ impl RunscInteractiveSessionExecutor {
|
|||
impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
||||
fn start(
|
||||
&self,
|
||||
request: &InteractiveSessionRequest,
|
||||
request: &InteractiveSessionRequestV2,
|
||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
||||
if request.protocol_version != ProtocolVersion::V1
|
||||
if request.protocol_version != InteractiveProtocolVersion::V2
|
||||
|| request.initial_size.session_id != request.session_id
|
||||
{
|
||||
return Err(ControlError::InvalidRequest);
|
||||
|
|
@ -113,22 +114,13 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
|||
hex_session(request.session_id.as_bytes())
|
||||
)),
|
||||
argv: request.argv.clone(),
|
||||
initial_size: request.initial_size,
|
||||
initial_modes: request.initial_modes.clone(),
|
||||
};
|
||||
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
|
||||
engine
|
||||
.acquire(&acquisition)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
if let Err(error) = engine.handle(
|
||||
self.generation_id,
|
||||
SessionTerminalEvent::Resize(request.initial_size),
|
||||
) {
|
||||
let _ = engine.lose(
|
||||
request.session_id,
|
||||
self.generation_id,
|
||||
TerminalLoss::ClientDisconnected,
|
||||
);
|
||||
return Err(terminal_control_error(&error));
|
||||
}
|
||||
drop(engine);
|
||||
Ok(Box::new(RunscInteractiveControlSession {
|
||||
engine: Arc::clone(&self.engine),
|
||||
|
|
@ -137,6 +129,19 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
|||
session_id: request.session_id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn clean_after_sandbox_absence(&self) -> Result<(), ControlError> {
|
||||
self.engine
|
||||
.lock()
|
||||
.map_err(|_| ControlError::LockPoisoned)?
|
||||
.clean_after_sandbox_absence(
|
||||
self.generation_id,
|
||||
self.container_id,
|
||||
self.host_coordinator,
|
||||
lazaret_linux::process::match_recovery_process,
|
||||
)
|
||||
.map_err(|error| terminal_control_error(&error))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct RunscInteractiveControlSession {
|
||||
|
|
@ -149,11 +154,11 @@ pub(super) struct RunscInteractiveControlSession {
|
|||
impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ControlError> {
|
||||
event: InteractiveSessionEventV2,
|
||||
) -> Result<InteractiveSessionBatchV2, ControlError> {
|
||||
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
|
||||
let mut messages = match event {
|
||||
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(resize)) => {
|
||||
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(resize)) => {
|
||||
if self
|
||||
.prompt_transport
|
||||
.retain_prompt_resize(resize)
|
||||
|
|
@ -162,17 +167,17 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
Vec::new()
|
||||
} else {
|
||||
engine
|
||||
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
|
||||
.handle(self.generation_id, SessionTerminalEventV2::Resize(resize))
|
||||
.map_err(|error| terminal_control_error(&error))?
|
||||
}
|
||||
}
|
||||
InteractiveSessionEvent::Terminal(event) => engine
|
||||
InteractiveSessionEventV2::Terminal(event) => engine
|
||||
.handle(self.generation_id, event)
|
||||
.map_err(|error| terminal_control_error(&error))?,
|
||||
InteractiveSessionEvent::Poll { session_id } if session_id == self.session_id => {
|
||||
InteractiveSessionEventV2::Poll { session_id } if session_id == self.session_id => {
|
||||
Vec::new()
|
||||
}
|
||||
InteractiveSessionEvent::PromptDecision {
|
||||
InteractiveSessionEventV2::PromptDecision {
|
||||
session_id,
|
||||
signed_decision,
|
||||
} if session_id == self.session_id => {
|
||||
|
|
@ -190,7 +195,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
.map_err(|error| terminal_control_error(&error))?;
|
||||
if let Some(resize) = decision.resumed_size {
|
||||
engine
|
||||
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
|
||||
.handle(self.generation_id, SessionTerminalEventV2::Resize(resize))
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
}
|
||||
}
|
||||
|
|
@ -199,8 +204,8 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
.map_err(|error| terminal_control_error(&error))?;
|
||||
Vec::new()
|
||||
}
|
||||
InteractiveSessionEvent::PromptDecision { .. }
|
||||
| InteractiveSessionEvent::Poll { .. } => return Err(ControlError::InvalidRequest),
|
||||
InteractiveSessionEventV2::PromptDecision { .. }
|
||||
| InteractiveSessionEventV2::Poll { .. } => return Err(ControlError::InvalidRequest),
|
||||
};
|
||||
if let Some(prompt) = self
|
||||
.prompt_transport
|
||||
|
|
@ -210,7 +215,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
engine
|
||||
.set_route(self.session_id, self.generation_id, TerminalRoute::Prompt)
|
||||
.map_err(|error| terminal_control_error(&error))?;
|
||||
return Ok(InteractiveSessionBatch {
|
||||
return Ok(InteractiveSessionBatchV2 {
|
||||
session_id: self.session_id,
|
||||
messages: vec![InteractiveSessionMessage::Prompt(prompt)],
|
||||
});
|
||||
|
|
@ -225,7 +230,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
|||
.map_err(|error| terminal_control_error(&error))?,
|
||||
);
|
||||
}
|
||||
Ok(InteractiveSessionBatch {
|
||||
Ok(InteractiveSessionBatchV2 {
|
||||
session_id: self.session_id,
|
||||
messages: messages
|
||||
.into_iter()
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
use super::{
|
||||
Arc, ChildStandardIo, DeviceId, File, FileDescriptor, GenerationId, GuestProcessGroupId,
|
||||
GuestProcessId, GuestProcessIdentity, GuestProcessStartTimeTicks,
|
||||
HostCoordinatorTerminalRecorder, IDENTITY_DEADLINE, InodeId, Instant, Mutex, OFlags,
|
||||
OpenptFlags, OsString, POLL_INTERVAL, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
||||
PersistedHostProcessOwnership, PidfsProcessIdentityV1, ProcessExit, ProcessOwnershipCommitAck,
|
||||
ProcessOwnershipContext, PtyIdentity, RetainedProcess, SANDBOX_INIT_DESTINATION, SessionId,
|
||||
SessionTerminal, SessionTerminalFactory, SessionTerminalFields, SessionTerminalProcess,
|
||||
SessionTerminalState, StagedProcessOwnership, TargetCommand, TerminalAcquisition,
|
||||
TerminalError, TerminalResize, Winsize, child_broker, current_process_identity, fcntl_getfl,
|
||||
fcntl_setfl, fstat, grantpt, io, ioctl_tiocgptpeer, openpt, release_physical, tcsetwinsize,
|
||||
unlockpt,
|
||||
Arc, ChildStandardIo, ControlModes, DeviceId, File, FileDescriptor, GenerationId,
|
||||
GuestProcessGroupId, GuestProcessId, GuestProcessIdentity, GuestProcessStartTimeTicks,
|
||||
HostCoordinatorTerminalRecorder, IDENTITY_DEADLINE, InodeId, InputModes, Instant, LocalModes,
|
||||
Mutex, OFlags, OpenptFlags, OptionalActions, OsString, OutputModes, POLL_INTERVAL,
|
||||
PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE, PersistedHostProcessOwnership,
|
||||
PidfsProcessIdentityV1, ProcessExit, ProcessOwnershipCommitAck, ProcessOwnershipContext,
|
||||
PtyIdentity, RetainedProcess, SANDBOX_INIT_DESTINATION, SessionId, SessionTerminal,
|
||||
SessionTerminalFactory, SessionTerminalFields, SessionTerminalProcess, SessionTerminalState,
|
||||
SpecialCodeIndex, StagedProcessOwnership, TargetCommand, TerminalAcquisition, TerminalError,
|
||||
TerminalModesV2, TerminalResize, Winsize, child_broker, current_process_identity, fcntl_getfl,
|
||||
fcntl_setfl, fstat, grantpt, io, ioctl_tiocgptpeer, openpt, release_physical, tcgetattr,
|
||||
tcsendbreak, tcsetattr, tcsetwinsize, unlockpt,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::AsRawFd;
|
||||
|
|
@ -40,6 +41,12 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
|
|||
return Err(TerminalError::AuthorityMismatch);
|
||||
}
|
||||
let (master, slave, pty) = allocate_pty()?;
|
||||
if let Err(error) = configure_pty(&slave, &request.initial_modes, request.initial_size) {
|
||||
drop(master);
|
||||
drop(slave);
|
||||
recorder.clean(clean_terminal(request.generation_id, request.session_id)?)?;
|
||||
return Err(error.into());
|
||||
}
|
||||
remove_pid_file(&request.internal_pid_file)?;
|
||||
let mut target = TargetCommand::new(request.runsc_program.clone());
|
||||
for argument in &request.runsc_global_arguments {
|
||||
|
|
@ -308,6 +315,15 @@ impl SessionTerminalProcess for RunscSessionTerminal {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn send_break(&self) -> io::Result<()> {
|
||||
tcsendbreak(
|
||||
self.master
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::other("PTY is closed"))?,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||
self.master
|
||||
.as_mut()
|
||||
|
|
@ -338,12 +354,13 @@ impl SessionTerminalProcess for RunscSessionTerminal {
|
|||
}
|
||||
|
||||
fn group_absent(&self) -> Result<bool, TerminalError> {
|
||||
if observe_guest_identity(&self.request, self.guest.leader_pid.get())?
|
||||
.is_some_and(|observed| observed != self.guest)
|
||||
{
|
||||
let leader_matches = observe_guest_identity(&self.request, self.guest.leader_pid.get())?
|
||||
.is_none_or(|observed| observed == self.guest);
|
||||
let group_absent = observe_group_absent(&self.request, self.guest.process_group_id.get())?;
|
||||
if !leader_matches && !group_absent {
|
||||
return Err(TerminalError::GuestIdentityMismatch);
|
||||
}
|
||||
observe_group_absent(&self.request, self.guest.process_group_id.get())
|
||||
Ok(group_absent)
|
||||
}
|
||||
|
||||
fn terminate_exec(&self) -> io::Result<()> {
|
||||
|
|
@ -393,6 +410,60 @@ fn allocate_pty() -> io::Result<(File, File, PtyIdentity)> {
|
|||
Ok((File::from(master), File::from(slave), pty))
|
||||
}
|
||||
|
||||
fn configure_pty(slave: &File, modes: &TerminalModesV2, size: TerminalResize) -> io::Result<()> {
|
||||
if modes.line_discipline != 0 {
|
||||
return Err(io::Error::other("unsupported terminal line discipline"));
|
||||
}
|
||||
let mut configured = tcgetattr(slave)?;
|
||||
configured.input_modes = InputModes::from_bits(modes.input_modes)
|
||||
.ok_or_else(|| io::Error::other("unsupported terminal input modes"))?;
|
||||
configured.output_modes = OutputModes::from_bits(modes.output_modes)
|
||||
.ok_or_else(|| io::Error::other("unsupported terminal output modes"))?;
|
||||
configured.control_modes = ControlModes::from_bits(modes.control_modes)
|
||||
.ok_or_else(|| io::Error::other("unsupported terminal control modes"))?;
|
||||
configured.local_modes = LocalModes::from_bits(modes.local_modes)
|
||||
.ok_or_else(|| io::Error::other("unsupported terminal local modes"))?;
|
||||
configured.line_discipline = modes.line_discipline;
|
||||
configured.set_input_speed(modes.input_speed)?;
|
||||
configured.set_output_speed(modes.output_speed)?;
|
||||
for (index, value) in terminal_control_codes(modes) {
|
||||
configured.special_codes[index] = value;
|
||||
}
|
||||
tcsetattr(slave, OptionalActions::Now, &configured)?;
|
||||
tcsetwinsize(
|
||||
slave,
|
||||
Winsize {
|
||||
ws_row: size.rows.get(),
|
||||
ws_col: size.columns.get(),
|
||||
ws_xpixel: size.pixel_width,
|
||||
ws_ypixel: size.pixel_height,
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn terminal_control_codes(modes: &TerminalModesV2) -> [(SpecialCodeIndex, u8); 17] {
|
||||
[
|
||||
(SpecialCodeIndex::VINTR, modes.interrupt),
|
||||
(SpecialCodeIndex::VQUIT, modes.quit),
|
||||
(SpecialCodeIndex::VERASE, modes.erase),
|
||||
(SpecialCodeIndex::VKILL, modes.kill),
|
||||
(SpecialCodeIndex::VEOF, modes.eof),
|
||||
(SpecialCodeIndex::VTIME, modes.time),
|
||||
(SpecialCodeIndex::VMIN, modes.minimum),
|
||||
(SpecialCodeIndex::VSWTC, modes.switch),
|
||||
(SpecialCodeIndex::VSTART, modes.start),
|
||||
(SpecialCodeIndex::VSTOP, modes.stop),
|
||||
(SpecialCodeIndex::VSUSP, modes.suspend),
|
||||
(SpecialCodeIndex::VEOL, modes.end_of_line),
|
||||
(SpecialCodeIndex::VREPRINT, modes.reprint),
|
||||
(SpecialCodeIndex::VDISCARD, modes.discard),
|
||||
(SpecialCodeIndex::VWERASE, modes.word_erase),
|
||||
(SpecialCodeIndex::VLNEXT, modes.literal_next),
|
||||
(SpecialCodeIndex::VEOL2, modes.end_of_line_2),
|
||||
]
|
||||
}
|
||||
|
||||
pub(super) fn staged_terminal(
|
||||
request: &TerminalAcquisition,
|
||||
pty: PtyIdentity,
|
||||
|
|
@ -619,3 +690,71 @@ fn hex_lower(bytes: &[u8]) -> String {
|
|||
output
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod configuration_tests {
|
||||
use super::*;
|
||||
use rustix::termios::tcgetwinsize;
|
||||
|
||||
#[test]
|
||||
fn pty_modes_and_size_are_applied_before_guest_start() {
|
||||
let (_master, slave, _) = allocate_pty().unwrap();
|
||||
let mut original = tcgetattr(&slave).unwrap();
|
||||
original.set_input_speed(original.input_speed()).unwrap();
|
||||
original.set_output_speed(original.output_speed()).unwrap();
|
||||
let mut modes = modes_from(&original);
|
||||
modes.local_modes ^= LocalModes::ECHO.bits();
|
||||
modes.interrupt = 7;
|
||||
modes.erase = 8;
|
||||
let size = TerminalResize {
|
||||
session_id: SessionId::new([1; 32]),
|
||||
rows: 55.try_into().unwrap(),
|
||||
columns: 144.try_into().unwrap(),
|
||||
pixel_width: 901,
|
||||
pixel_height: 502,
|
||||
};
|
||||
|
||||
configure_pty(&slave, &modes, size).unwrap();
|
||||
|
||||
let configured = tcgetattr(&slave).unwrap();
|
||||
assert_eq!(modes_from(&configured), modes);
|
||||
assert_eq!(
|
||||
tcgetwinsize(&slave).unwrap(),
|
||||
Winsize {
|
||||
ws_row: 55,
|
||||
ws_col: 144,
|
||||
ws_xpixel: 901,
|
||||
ws_ypixel: 502,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn modes_from(modes: &rustix::termios::Termios) -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: modes.input_modes.bits(),
|
||||
output_modes: modes.output_modes.bits(),
|
||||
control_modes: modes.control_modes.bits(),
|
||||
local_modes: modes.local_modes.bits(),
|
||||
line_discipline: modes.line_discipline,
|
||||
input_speed: modes.input_speed(),
|
||||
output_speed: modes.output_speed(),
|
||||
interrupt: modes.special_codes[SpecialCodeIndex::VINTR],
|
||||
quit: modes.special_codes[SpecialCodeIndex::VQUIT],
|
||||
erase: modes.special_codes[SpecialCodeIndex::VERASE],
|
||||
kill: modes.special_codes[SpecialCodeIndex::VKILL],
|
||||
eof: modes.special_codes[SpecialCodeIndex::VEOF],
|
||||
time: modes.special_codes[SpecialCodeIndex::VTIME],
|
||||
minimum: modes.special_codes[SpecialCodeIndex::VMIN],
|
||||
switch: modes.special_codes[SpecialCodeIndex::VSWTC],
|
||||
start: modes.special_codes[SpecialCodeIndex::VSTART],
|
||||
stop: modes.special_codes[SpecialCodeIndex::VSTOP],
|
||||
suspend: modes.special_codes[SpecialCodeIndex::VSUSP],
|
||||
end_of_line: modes.special_codes[SpecialCodeIndex::VEOL],
|
||||
reprint: modes.special_codes[SpecialCodeIndex::VREPRINT],
|
||||
discard: modes.special_codes[SpecialCodeIndex::VDISCARD],
|
||||
word_erase: modes.special_codes[SpecialCodeIndex::VWERASE],
|
||||
literal_next: modes.special_codes[SpecialCodeIndex::VLNEXT],
|
||||
end_of_line_2: modes.special_codes[SpecialCodeIndex::VEOL2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ use lazaret_core::identity::{
|
|||
use lazaret_core::protocol::{VerifiedSession, encode_signed, issue_session_capability};
|
||||
use lazaret_core::records::{
|
||||
DeveloperDecisionKind, Domain, DurableDecision, Generation, GenerationIdentity, MailboxRequest,
|
||||
Participant, SandboxOwnership, Session, SessionBinding, TerminalInput, TerminalSignal,
|
||||
Participant, ProtocolVersion, SandboxOwnership, Session, SessionBinding, TerminalBreak,
|
||||
TerminalInput,
|
||||
};
|
||||
use lazaret_linux::filesystem::try_exclusive_flock;
|
||||
use tempfile::TempDir;
|
||||
|
|
@ -352,6 +353,17 @@ impl SessionTerminalProcess for MockProcess {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn send_break(&self) -> io::Result<()> {
|
||||
self.state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push("break".into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let Some(output) = state.output.pop_front() else {
|
||||
|
|
@ -399,7 +411,7 @@ impl SessionTerminalProcess for MockProcess {
|
|||
if let Some(hook) = hook {
|
||||
hook();
|
||||
}
|
||||
if identity_mismatch {
|
||||
if identity_mismatch && !group_absent {
|
||||
return Err(TerminalError::GuestIdentityMismatch);
|
||||
}
|
||||
Ok(group_absent)
|
||||
|
|
@ -529,6 +541,43 @@ fn acquisition(session_id: SessionId) -> TerminalAcquisition {
|
|||
environment: BTreeMap::new(),
|
||||
internal_pid_file: "/tmp/not-used-by-mock".into(),
|
||||
argv: vec![b"/bin/agent".to_vec()],
|
||||
initial_size: TerminalResize {
|
||||
session_id,
|
||||
rows: NonZeroU16::new(24).unwrap(),
|
||||
columns: NonZeroU16::new(80).unwrap(),
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
},
|
||||
initial_modes: terminal_modes(),
|
||||
}
|
||||
}
|
||||
|
||||
fn terminal_modes() -> TerminalModesV2 {
|
||||
TerminalModesV2 {
|
||||
input_modes: 0,
|
||||
output_modes: 0,
|
||||
control_modes: 0,
|
||||
local_modes: 0,
|
||||
line_discipline: 0,
|
||||
input_speed: 38_400,
|
||||
output_speed: 38_400,
|
||||
interrupt: 3,
|
||||
quit: 28,
|
||||
erase: 127,
|
||||
kill: 21,
|
||||
eof: 4,
|
||||
time: 0,
|
||||
minimum: 1,
|
||||
switch: 0,
|
||||
start: 17,
|
||||
stop: 19,
|
||||
suspend: 26,
|
||||
end_of_line: 0,
|
||||
reprint: 18,
|
||||
discard: 15,
|
||||
word_erase: 23,
|
||||
literal_next: 22,
|
||||
end_of_line_2: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue