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::fmt;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
use std::net::Shutdown;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::{Mutex, mpsc};
|
||||||
|
use std::thread::JoinHandle;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
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::identity::{ClientId, SessionAnchorId, SessionId};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
ClientEnrollmentResponse, ControlRequest, ControlResponse, DeveloperDecision,
|
ClientEnrollmentResponse, ControlRequest, ControlResponse, DeveloperDecision,
|
||||||
DeveloperDecisionKind, InteractiveSessionBatch, InteractiveSessionEvent,
|
DeveloperDecisionKind, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||||
InteractiveSessionRequest, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
InteractiveSessionRequestV2, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||||
ProtocolPublicationIdentity, ProtocolVersion, SessionBinding, SessionExitOutcome,
|
ProtocolPublicationIdentity, ProtocolVersion, SessionBinding, SessionExitOutcome,
|
||||||
SessionRegistrationRequest, TlsSubjectId,
|
SessionRegistrationRequest, TlsSubjectId,
|
||||||
};
|
};
|
||||||
|
|
@ -414,9 +416,9 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
||||||
let cli_id = identity.cli_id;
|
let cli_id = identity.cli_id;
|
||||||
let session_id = identity.session_id;
|
let session_id = identity.session_id;
|
||||||
let pending = PendingClientEnrollment::create(generation.clone(), cli_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 =
|
let mut enrollment =
|
||||||
ControlClient::connect(&endpoint.socket_path, anonymous, &endpoint.publication)?;
|
ControlClient::connect_v2(&endpoint.socket_path, anonymous, &endpoint.publication)?;
|
||||||
ControlClient::send(
|
ControlClient::send(
|
||||||
&mut enrollment,
|
&mut enrollment,
|
||||||
&ControlRequest::Enroll(pending.request().clone()),
|
&ControlRequest::Enroll(pending.request().clone()),
|
||||||
|
|
@ -442,12 +444,12 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
||||||
session_id,
|
session_id,
|
||||||
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
|
session_anchor_public_key: identity.anchor.verifying_key().to_bytes().to_vec(),
|
||||||
};
|
};
|
||||||
let authenticated = ControlClient::config(
|
let authenticated = ControlClient::config_v2(
|
||||||
&endpoint.publication,
|
&endpoint.publication,
|
||||||
Some((&credential, pending.private_key())),
|
Some((&credential, pending.private_key())),
|
||||||
)?;
|
)?;
|
||||||
let mut stream =
|
let mut stream =
|
||||||
ControlClient::connect(&endpoint.socket_path, authenticated, &endpoint.publication)?;
|
ControlClient::connect_v2(&endpoint.socket_path, authenticated, &endpoint.publication)?;
|
||||||
ControlClient::send(
|
ControlClient::send(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
&ControlRequest::Register(SessionRegistrationRequest {
|
&ControlRequest::Register(SessionRegistrationRequest {
|
||||||
|
|
@ -466,13 +468,70 @@ pub fn execute_interactive_over_control_face_as<E, H: HostTerminal>(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
endpoint.acknowledge_waiter_transfer()?;
|
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)
|
run_interactive_session(&mut transport, terminal, identity, &binding, argv)
|
||||||
.map_err(widen_client_error)
|
.map_err(widen_client_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ControlFaceInteractiveTransport {
|
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 {
|
impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
|
||||||
|
|
@ -480,27 +539,43 @@ impl InteractiveSessionTransport for ControlFaceInteractiveTransport {
|
||||||
|
|
||||||
fn start(
|
fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: InteractiveSessionRequest,
|
request: InteractiveSessionRequestV2,
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
) -> Result<(), ClientError<Self::Error>> {
|
||||||
ControlClient::send(
|
self.submit(InteractiveTransportCommand::Start(request))
|
||||||
&mut self.stream,
|
|
||||||
&ControlRequest::ExecuteInteractive(request),
|
|
||||||
)?;
|
|
||||||
receive_interactive_batch(&mut self.stream)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exchange(
|
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>> {
|
||||||
|
self.submit(InteractiveTransportCommand::Event(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_receive(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: InteractiveSessionEvent,
|
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>> {
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
match self.batches.try_recv() {
|
||||||
ControlClient::send(&mut self.stream, &event).map_err(map_control_connection)?;
|
Ok(batch) => batch.map(Some),
|
||||||
receive_interactive_batch(&mut self.stream)
|
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(
|
fn receive_interactive_batch(
|
||||||
stream: &mut impl std::io::Read,
|
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)
|
ControlClient::receive(stream).map_err(map_control_connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,21 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::num::NonZeroU16;
|
use std::num::NonZeroU16;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
DeveloperDecisionKind, InteractiveSessionBatch, InteractiveSessionEvent,
|
DeveloperDecisionKind, InteractiveProtocolVersion, InteractiveSessionBatchV2,
|
||||||
InteractiveSessionMessage, InteractiveSessionRequest, ProtocolVersion, SessionBinding,
|
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequestV2,
|
||||||
SessionClose, SessionExitOutcome, SessionTerminalEvent, SessionTerminalMessage, TerminalInput,
|
SessionBinding, SessionExitOutcome, SessionTerminalEventV2, SessionTerminalMessage,
|
||||||
TerminalPrompt, TerminalResize, TerminalSignal, TerminalSignalKind,
|
TerminalBreak, TerminalInput, TerminalModesV2, TerminalPrompt, TerminalResize,
|
||||||
};
|
};
|
||||||
use nix::sys::signal::{SigSet, SigmaskHow, Signal};
|
use nix::sys::signal::{SigSet, SigmaskHow, Signal};
|
||||||
use nix::sys::signalfd::{SfdFlags, SignalFd};
|
use nix::sys::signalfd::{SfdFlags, SignalFd};
|
||||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
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};
|
use crate::{ClientError, ClientSessionIdentity};
|
||||||
|
|
||||||
|
|
@ -23,21 +26,26 @@ pub trait InteractiveSessionTransport {
|
||||||
/// Returns an authenticated session or framing failure.
|
/// Returns an authenticated session or framing failure.
|
||||||
fn start(
|
fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: InteractiveSessionRequest,
|
request: InteractiveSessionRequestV2,
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>>;
|
) -> Result<(), ClientError<Self::Error>>;
|
||||||
|
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns a lost connection, stale session, or framing failure.
|
/// 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,
|
&mut self,
|
||||||
event: InteractiveSessionEvent,
|
) -> Result<Option<InteractiveSessionBatchV2>, ClientError<Self::Error>>;
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>>;
|
|
||||||
|
fn cancel(&mut self);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum HostTerminalEvent {
|
pub enum HostTerminalEvent {
|
||||||
Input(Vec<u8>),
|
Input(Vec<u8>),
|
||||||
Signal(TerminalSignalKind),
|
Break,
|
||||||
Resize,
|
Resize,
|
||||||
LocalSuspend,
|
LocalSuspend,
|
||||||
Terminate,
|
Terminate,
|
||||||
|
|
@ -54,6 +62,7 @@ pub trait HostTerminal {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error when the current terminal size cannot be observed.
|
/// Returns an error when the current terminal size cannot be observed.
|
||||||
fn size(&self, session_id: lazaret_core::identity::SessionId) -> io::Result<TerminalResize>;
|
fn size(&self, session_id: lazaret_core::identity::SessionId) -> io::Result<TerminalResize>;
|
||||||
|
fn modes(&self) -> TerminalModesV2;
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error while observing terminal input or signals.
|
/// Returns an error while observing terminal input or signals.
|
||||||
fn next_event(&mut self) -> io::Result<HostTerminalEvent>;
|
fn next_event(&mut self) -> io::Result<HostTerminalEvent>;
|
||||||
|
|
@ -80,6 +89,9 @@ pub struct SystemTerminal {
|
||||||
original_flags: OFlags,
|
original_flags: OFlags,
|
||||||
raw: bool,
|
raw: bool,
|
||||||
signals: SignalQueue,
|
signals: SignalQueue,
|
||||||
|
events: VecDeque<HostTerminalEvent>,
|
||||||
|
line_start: bool,
|
||||||
|
escape_pending: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemTerminal {
|
impl SystemTerminal {
|
||||||
|
|
@ -95,8 +107,59 @@ impl SystemTerminal {
|
||||||
terminal,
|
terminal,
|
||||||
raw: false,
|
raw: false,
|
||||||
signals: SignalQueue::register()?,
|
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 {
|
impl HostTerminal for SystemTerminal {
|
||||||
|
|
@ -106,7 +169,6 @@ impl HostTerminal for SystemTerminal {
|
||||||
}
|
}
|
||||||
let mut raw = self.original.clone();
|
let mut raw = self.original.clone();
|
||||||
raw.make_raw();
|
raw.make_raw();
|
||||||
raw.local_modes.insert(LocalModes::ISIG);
|
|
||||||
tcsetattr(&self.terminal, OptionalActions::Flush, &raw)?;
|
tcsetattr(&self.terminal, OptionalActions::Flush, &raw)?;
|
||||||
if let Err(error) = fcntl_setfl(&self.terminal, self.original_flags | OFlags::NONBLOCK) {
|
if let Err(error) = fcntl_setfl(&self.terminal, self.original_flags | OFlags::NONBLOCK) {
|
||||||
let _ = tcsetattr(&self.terminal, OptionalActions::Flush, &self.original);
|
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> {
|
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()? {
|
if let Some(event) = self.signals.next_event()? {
|
||||||
return Ok(event);
|
return Ok(event);
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +217,8 @@ impl HostTerminal for SystemTerminal {
|
||||||
Ok(0) => Ok(HostTerminalEvent::Terminate),
|
Ok(0) => Ok(HostTerminalEvent::Terminate),
|
||||||
Ok(length) => {
|
Ok(length) => {
|
||||||
bytes.truncate(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 => {
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
std::thread::sleep(Duration::from_millis(5));
|
||||||
|
|
@ -196,9 +266,7 @@ impl HostTerminal for SystemTerminal {
|
||||||
resumed_size = Some(self.size(prompt.session_id)?);
|
resumed_size = Some(self.size(prompt.session_id)?);
|
||||||
}
|
}
|
||||||
HostTerminalEvent::Terminate => return Err(io::ErrorKind::Interrupted.into()),
|
HostTerminalEvent::Terminate => return Err(io::ErrorKind::Interrupted.into()),
|
||||||
HostTerminalEvent::Idle
|
HostTerminalEvent::Idle | HostTerminalEvent::Resize | HostTerminalEvent::Break => {}
|
||||||
| HostTerminalEvent::Resize
|
|
||||||
| HostTerminalEvent::Signal(_) => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -259,11 +327,11 @@ impl SignalQueue {
|
||||||
};
|
};
|
||||||
let signal = Signal::try_from(info.ssi_signo.cast_signed()).map_err(errno_to_io)?;
|
let signal = Signal::try_from(info.ssi_signo.cast_signed()).map_err(errno_to_io)?;
|
||||||
Ok(match signal {
|
Ok(match signal {
|
||||||
Signal::SIGINT => Some(HostTerminalEvent::Signal(TerminalSignalKind::Interrupt)),
|
Signal::SIGINT | Signal::SIGQUIT | Signal::SIGTERM | Signal::SIGHUP => {
|
||||||
Signal::SIGQUIT => Some(HostTerminalEvent::Signal(TerminalSignalKind::Quit)),
|
Some(HostTerminalEvent::Terminate)
|
||||||
|
}
|
||||||
Signal::SIGTSTP => Some(HostTerminalEvent::LocalSuspend),
|
Signal::SIGTSTP => Some(HostTerminalEvent::LocalSuspend),
|
||||||
Signal::SIGWINCH => Some(HostTerminalEvent::Resize),
|
Signal::SIGWINCH => Some(HostTerminalEvent::Resize),
|
||||||
Signal::SIGTERM | Signal::SIGHUP => Some(HostTerminalEvent::Terminate),
|
|
||||||
_ => None,
|
_ => 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 {
|
fn errno_to_io(error: nix::errno::Errno) -> io::Error {
|
||||||
io::Error::from_raw_os_error(error as i32)
|
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 result = (|| {
|
||||||
let session_id = identity.session_id();
|
let session_id = identity.session_id();
|
||||||
let initial_size = terminal.size(session_id)?;
|
let initial_size = terminal.size(session_id)?;
|
||||||
let mut batch = transport.start(InteractiveSessionRequest {
|
transport.start(InteractiveSessionRequestV2 {
|
||||||
protocol_version: ProtocolVersion::V1,
|
protocol_version: InteractiveProtocolVersion::V2,
|
||||||
session_id,
|
session_id,
|
||||||
argv: argv.to_vec(),
|
argv: argv.to_vec(),
|
||||||
initial_size,
|
initial_size,
|
||||||
|
initial_modes: terminal.modes(),
|
||||||
})?;
|
})?;
|
||||||
|
let mut pending = VecDeque::new();
|
||||||
loop {
|
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() {
|
return match exit.outcome() {
|
||||||
SessionExitOutcome::Exited => exit
|
SessionExitOutcome::Exited => exit
|
||||||
.exit_code()
|
.exit_code()
|
||||||
|
|
@ -318,46 +419,60 @@ pub fn run_interactive_session<T: InteractiveSessionTransport, H: HostTerminal>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let event =
|
for event in handled.events {
|
||||||
match terminal.next_event()? {
|
enqueue_event(&mut pending, event)?;
|
||||||
HostTerminalEvent::Input(bytes) => InteractiveSessionEvent::Terminal(
|
}
|
||||||
SessionTerminalEvent::Input(TerminalInput { session_id, bytes }),
|
} else {
|
||||||
),
|
let event = terminal.next_event()?;
|
||||||
HostTerminalEvent::Signal(signal) => InteractiveSessionEvent::Terminal(
|
match host_event(terminal, session_id, event)? {
|
||||||
SessionTerminalEvent::Signal(TerminalSignal { session_id, signal }),
|
HostEvent::Remote(event) => enqueue_event(&mut pending, event)?,
|
||||||
),
|
HostEvent::Continue => {}
|
||||||
HostTerminalEvent::Resize => InteractiveSessionEvent::Terminal(
|
HostEvent::Disconnect => {
|
||||||
SessionTerminalEvent::Resize(terminal.size(session_id)?),
|
transport.cancel();
|
||||||
),
|
return Err(ClientError::LostConnection);
|
||||||
HostTerminalEvent::LocalSuspend => {
|
}
|
||||||
terminal.suspend_and_resume()?;
|
}
|
||||||
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(
|
continue;
|
||||||
terminal.size(session_id)?,
|
}
|
||||||
))
|
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);
|
let restoration = terminal.restore().map_err(ClientError::Io);
|
||||||
restoration?;
|
restoration?;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_batch<T: InteractiveSessionTransport, H: HostTerminal>(
|
struct HandledBatch {
|
||||||
transport: &mut T,
|
exit: Option<lazaret_core::records::SessionExit>,
|
||||||
|
events: Vec<InteractiveSessionEventV2>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_batch<H: HostTerminal, E>(
|
||||||
terminal: &mut H,
|
terminal: &mut H,
|
||||||
identity: &ClientSessionIdentity,
|
identity: &ClientSessionIdentity,
|
||||||
binding: &SessionBinding,
|
binding: &SessionBinding,
|
||||||
batch: InteractiveSessionBatch,
|
batch: InteractiveSessionBatchV2,
|
||||||
) -> Result<Option<lazaret_core::records::SessionExit>, ClientError<T::Error>> {
|
) -> Result<HandledBatch, ClientError<E>> {
|
||||||
if batch.session_id != identity.session_id() {
|
if batch.session_id != identity.session_id() {
|
||||||
return Err(ClientError::Protocol("response Session ID differs"));
|
return Err(ClientError::Protocol("response Session ID differs"));
|
||||||
}
|
}
|
||||||
|
let mut events = Vec::new();
|
||||||
for message in batch.messages {
|
for message in batch.messages {
|
||||||
match message {
|
match message {
|
||||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Output(output)) => {
|
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Output(output)) => {
|
||||||
|
|
@ -366,35 +481,92 @@ fn handle_batch<T: InteractiveSessionTransport, H: HostTerminal>(
|
||||||
}
|
}
|
||||||
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(exit)) => {
|
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(exit)) => {
|
||||||
require_session(exit.session_id(), identity.session_id())?;
|
require_session(exit.session_id(), identity.session_id())?;
|
||||||
return Ok(Some(exit));
|
return Ok(HandledBatch {
|
||||||
|
exit: Some(exit),
|
||||||
|
events,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
InteractiveSessionMessage::Prompt(prompt) => {
|
InteractiveSessionMessage::Prompt(prompt) => {
|
||||||
require_session(prompt.session_id, identity.session_id())?;
|
require_session(prompt.session_id, identity.session_id())?;
|
||||||
let outcome = terminal.prompt(&prompt)?;
|
let outcome = terminal.prompt(&prompt)?;
|
||||||
if let Some(resize) = outcome.resumed_size {
|
if let Some(resize) = outcome.resumed_size {
|
||||||
let response = transport.exchange(InteractiveSessionEvent::Terminal(
|
events.push(InteractiveSessionEventV2::Terminal(
|
||||||
SessionTerminalEvent::Resize(resize),
|
SessionTerminalEventV2::Resize(resize),
|
||||||
))?;
|
));
|
||||||
if let Some(exit) =
|
|
||||||
handle_batch(transport, terminal, identity, binding, response)?
|
|
||||||
{
|
|
||||||
return Ok(Some(exit));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let signed_decision =
|
let signed_decision =
|
||||||
identity.sign_developer_decision(binding.clone(), &prompt, outcome.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(),
|
session_id: identity.session_id(),
|
||||||
signed_decision,
|
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>(
|
fn require_session<E>(
|
||||||
|
|
@ -426,10 +598,81 @@ fn hex(bytes: &[u8]) -> String {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::sys::signal::{SigSet, SigmaskHow, Signal, kill};
|
||||||
use nix::unistd::Pid;
|
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]
|
#[test]
|
||||||
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
#[allow(clippy::disallowed_methods, clippy::disallowed_types)]
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,14 @@ use lazaret_core::identity::{
|
||||||
SandboxId, SessionId,
|
SandboxId, SessionId,
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatch,
|
DeveloperDecision, DeveloperDecisionKind, GenerationIdentity, InteractiveSessionBatchV2,
|
||||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, SessionBinding,
|
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequestV2,
|
||||||
SessionExit, SessionTerminalMessage, SignedProtocolRecord, TerminalOutput, TerminalPrompt,
|
SessionBinding, SessionExit, SessionTerminalEventV2, SessionTerminalMessage,
|
||||||
TerminalResize, TerminalSignalKind,
|
SignedProtocolRecord, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||||
};
|
};
|
||||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
|
||||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
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 {
|
struct PtyTerminal {
|
||||||
_master: File,
|
_master: File,
|
||||||
|
|
@ -74,7 +74,6 @@ impl HostTerminal for PtyTerminal {
|
||||||
fn enter_raw(&mut self) -> io::Result<()> {
|
fn enter_raw(&mut self) -> io::Result<()> {
|
||||||
let mut raw = self.original.clone();
|
let mut raw = self.original.clone();
|
||||||
raw.make_raw();
|
raw.make_raw();
|
||||||
raw.local_modes.insert(LocalModes::ISIG);
|
|
||||||
tcsetattr(&self.slave, OptionalActions::Now, &raw)?;
|
tcsetattr(&self.slave, OptionalActions::Now, &raw)?;
|
||||||
fcntl_setfl(&self.slave, self.original_flags | OFlags::NONBLOCK)?;
|
fcntl_setfl(&self.slave, self.original_flags | OFlags::NONBLOCK)?;
|
||||||
self.raw = true;
|
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> {
|
fn next_event(&mut self) -> io::Result<HostTerminalEvent> {
|
||||||
Ok(self.events.pop_front().unwrap_or(HostTerminalEvent::Idle))
|
Ok(self.events.pop_front().unwrap_or(HostTerminalEvent::Idle))
|
||||||
}
|
}
|
||||||
|
|
@ -139,11 +142,14 @@ enum TransportFailure {
|
||||||
|
|
||||||
struct FixtureTransport {
|
struct FixtureTransport {
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
events: Vec<InteractiveSessionEvent>,
|
events: Vec<InteractiveSessionEventV2>,
|
||||||
prompt_sent: bool,
|
prompt_sent: bool,
|
||||||
failure: Option<TransportFailure>,
|
failure: Option<TransportFailure>,
|
||||||
signed_decision: Option<Vec<u8>>,
|
signed_decision: Option<Vec<u8>>,
|
||||||
prompt_after_events: usize,
|
prompt_after_events: usize,
|
||||||
|
batches: VecDeque<InteractiveSessionBatchV2>,
|
||||||
|
cancelled: bool,
|
||||||
|
blocked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FixtureTransport {
|
impl FixtureTransport {
|
||||||
|
|
@ -155,6 +161,9 @@ impl FixtureTransport {
|
||||||
failure: None,
|
failure: None,
|
||||||
signed_decision: None,
|
signed_decision: None,
|
||||||
prompt_after_events: 5,
|
prompt_after_events: 5,
|
||||||
|
batches: VecDeque::new(),
|
||||||
|
cancelled: false,
|
||||||
|
blocked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,13 +173,17 @@ impl InteractiveSessionTransport for FixtureTransport {
|
||||||
|
|
||||||
fn start(
|
fn start(
|
||||||
&mut self,
|
&mut self,
|
||||||
request: InteractiveSessionRequest,
|
request: InteractiveSessionRequestV2,
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
) -> Result<(), ClientError<Self::Error>> {
|
||||||
if matches!(self.failure, Some(TransportFailure::Stale)) {
|
if matches!(self.failure, Some(TransportFailure::Stale)) {
|
||||||
return Err(ClientError::StaleCoordinator);
|
return Err(ClientError::StaleCoordinator);
|
||||||
}
|
}
|
||||||
assert_eq!(request.session_id, self.session_id);
|
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,
|
self.session_id,
|
||||||
vec![InteractiveSessionMessage::Terminal(
|
vec![InteractiveSessionMessage::Terminal(
|
||||||
SessionTerminalMessage::Output(TerminalOutput {
|
SessionTerminalMessage::Output(TerminalOutput {
|
||||||
|
|
@ -178,32 +191,31 @@ impl InteractiveSessionTransport for FixtureTransport {
|
||||||
bytes: b"guest-output".to_vec(),
|
bytes: b"guest-output".to_vec(),
|
||||||
}),
|
}),
|
||||||
)],
|
)],
|
||||||
))
|
));
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exchange(
|
fn send(&mut self, event: InteractiveSessionEventV2) -> Result<(), ClientError<Self::Error>> {
|
||||||
&mut self,
|
|
||||||
event: InteractiveSessionEvent,
|
|
||||||
) -> Result<InteractiveSessionBatch, ClientError<Self::Error>> {
|
|
||||||
if matches!(self.failure, Some(TransportFailure::Lost)) {
|
if matches!(self.failure, Some(TransportFailure::Lost)) {
|
||||||
return Err(ClientError::LostConnection);
|
return Err(ClientError::LostConnection);
|
||||||
}
|
}
|
||||||
self.events.push(event.clone());
|
self.events.push(event.clone());
|
||||||
if let InteractiveSessionEvent::PromptDecision {
|
if let InteractiveSessionEventV2::PromptDecision {
|
||||||
signed_decision, ..
|
signed_decision, ..
|
||||||
} = event
|
} = event
|
||||||
{
|
{
|
||||||
self.signed_decision = Some(signed_decision);
|
self.signed_decision = Some(signed_decision);
|
||||||
return Ok(batch(
|
self.batches.push_back(batch(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
vec![InteractiveSessionMessage::Terminal(
|
vec![InteractiveSessionMessage::Terminal(
|
||||||
SessionTerminalMessage::Exit(SessionExit::exited(self.session_id, 23)),
|
SessionTerminalMessage::Exit(SessionExit::exited(self.session_id, 23)),
|
||||||
)],
|
)],
|
||||||
));
|
));
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
if !self.prompt_sent && self.events.len() >= self.prompt_after_events {
|
if !self.prompt_sent && self.events.len() >= self.prompt_after_events {
|
||||||
self.prompt_sent = true;
|
self.prompt_sent = true;
|
||||||
return Ok(batch(
|
self.batches.push_back(batch(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
vec![InteractiveSessionMessage::Prompt(TerminalPrompt {
|
vec![InteractiveSessionMessage::Prompt(TerminalPrompt {
|
||||||
session_id: self.session_id,
|
session_id: self.session_id,
|
||||||
|
|
@ -214,8 +226,23 @@ impl InteractiveSessionTransport for FixtureTransport {
|
||||||
justification: "update host metadata".into(),
|
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!(
|
assert!(matches!(
|
||||||
transport.events.as_slice(),
|
transport.events.as_slice(),
|
||||||
[
|
[
|
||||||
InteractiveSessionEvent::Poll { .. },
|
InteractiveSessionEventV2::Poll { .. },
|
||||||
InteractiveSessionEvent::Terminal(lazaret_core::records::SessionTerminalEvent::Resize(
|
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(_)),
|
||||||
_
|
InteractiveSessionEventV2::PromptDecision { .. }
|
||||||
)),
|
|
||||||
InteractiveSessionEvent::PromptDecision { .. }
|
|
||||||
]
|
]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 identity = ClientSessionIdentity::generate::<std::convert::Infallible>().unwrap();
|
||||||
let binding = binding(&identity);
|
let binding = binding(&identity);
|
||||||
let mut terminal = PtyTerminal::new([
|
let mut terminal = PtyTerminal::new([
|
||||||
HostTerminalEvent::Resize,
|
HostTerminalEvent::Resize,
|
||||||
HostTerminalEvent::Signal(TerminalSignalKind::Interrupt),
|
HostTerminalEvent::Input(vec![3]),
|
||||||
HostTerminalEvent::Signal(TerminalSignalKind::Quit),
|
HostTerminalEvent::Break,
|
||||||
HostTerminalEvent::LocalSuspend,
|
HostTerminalEvent::LocalSuspend,
|
||||||
HostTerminalEvent::Input(b"input".to_vec()),
|
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_eq!(decision.approved_command_argv, [b"/usr/bin/tool".to_vec()]);
|
||||||
assert!(transport.events.iter().any(|event| matches!(
|
assert!(transport.events.iter().any(|event| matches!(
|
||||||
event,
|
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!(
|
assert!(transport.events.iter().any(|event| matches!(
|
||||||
event,
|
event,
|
||||||
InteractiveSessionEvent::Terminal(
|
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Input(input))
|
||||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
if input.bytes == [3]
|
||||||
) if sent.signal == signal
|
|
||||||
)));
|
)));
|
||||||
}
|
assert!(transport.events.iter().any(|event| matches!(
|
||||||
assert!(!transport.events.iter().any(|event| matches!(
|
|
||||||
event,
|
event,
|
||||||
InteractiveSessionEvent::Terminal(
|
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Break(_))
|
||||||
lazaret_core::records::SessionTerminalEvent::Signal(sent)
|
|
||||||
) if sent.signal == TerminalSignalKind::Suspend
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 {
|
fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
||||||
SessionBinding {
|
SessionBinding {
|
||||||
generation_identity: GenerationIdentity {
|
generation_identity: GenerationIdentity {
|
||||||
|
|
@ -354,9 +402,38 @@ fn binding(identity: &ClientSessionIdentity) -> SessionBinding {
|
||||||
fn batch(
|
fn batch(
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
messages: Vec<InteractiveSessionMessage>,
|
messages: Vec<InteractiveSessionMessage>,
|
||||||
) -> InteractiveSessionBatch {
|
) -> InteractiveSessionBatchV2 {
|
||||||
InteractiveSessionBatch {
|
InteractiveSessionBatchV2 {
|
||||||
session_id,
|
session_id,
|
||||||
messages,
|
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};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
pub const CONTROL_ALPN: &[u8] = b"lazaret-control/1";
|
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_SERVER_NAME: &str = "coordinator.lazaret.invalid";
|
||||||
pub const CONTROL_SOCKET_NAME: &str = "c.sock";
|
pub const CONTROL_SOCKET_NAME: &str = "c.sock";
|
||||||
pub const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
pub const PUBLICATION_IDENTITY_NAME: &str = "protocol-identity.postcard";
|
||||||
|
|
@ -237,6 +238,25 @@ impl ControlClient {
|
||||||
pub fn config(
|
pub fn config(
|
||||||
publication: &ProtocolPublicationIdentity,
|
publication: &ProtocolPublicationIdentity,
|
||||||
client: Option<(&TlsCredential, PrivateKeyDer<'static>)>,
|
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> {
|
) -> Result<ClientConfig, ControlClientError> {
|
||||||
let mut roots = RootCertStore::empty();
|
let mut roots = RootCertStore::empty();
|
||||||
roots.add(CertificateDer::from(
|
roots.add(CertificateDer::from(
|
||||||
|
|
@ -258,7 +278,7 @@ impl ControlClient {
|
||||||
} else {
|
} else {
|
||||||
builder.with_no_client_auth()
|
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.enable_early_data = false;
|
||||||
config.resumption = rustls::client::Resumption::disabled();
|
config.resumption = rustls::client::Resumption::disabled();
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
|
@ -275,6 +295,33 @@ impl ControlClient {
|
||||||
) -> Result<
|
) -> Result<
|
||||||
rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>,
|
rustls::StreamOwned<ClientConnection, std::os::unix::net::UnixStream>,
|
||||||
ControlClientError,
|
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 socket = std::os::unix::net::UnixStream::connect(socket_path)?;
|
||||||
let server_name = ServerName::try_from(CONTROL_SERVER_NAME)
|
let server_name = ServerName::try_from(CONTROL_SERVER_NAME)
|
||||||
|
|
@ -288,7 +335,7 @@ impl ControlClient {
|
||||||
return Err(ControlClientError::InvalidCertificateProfile);
|
return Err(ControlClientError::InvalidCertificateProfile);
|
||||||
};
|
};
|
||||||
if connection.protocol_version() != Some(rustls::ProtocolVersion::TLSv1_3)
|
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
|
|| certificate_digest(certificate) != publication.coordinator_tls_certificate_sha256
|
||||||
{
|
{
|
||||||
return Err(ControlClientError::InvalidCertificateProfile);
|
return Err(ControlClientError::InvalidCertificateProfile);
|
||||||
|
|
|
||||||
|
|
@ -230,6 +230,7 @@ pub enum ControlRequest {
|
||||||
Register(SessionRegistrationRequest),
|
Register(SessionRegistrationRequest),
|
||||||
Execute(NonInteractiveSessionRequest),
|
Execute(NonInteractiveSessionRequest),
|
||||||
ExecuteInteractive(super::InteractiveSessionRequest),
|
ExecuteInteractive(super::InteractiveSessionRequest),
|
||||||
|
ExecuteInteractiveV2(super::InteractiveSessionRequestV2),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,28 @@ use super::{
|
||||||
DefaultAction, DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, Domain,
|
DefaultAction, DerivedNetworkPolicy, DeveloperDecision, DeveloperDecisionKind, Domain,
|
||||||
DomainStore, DomainStoreMetadata, EnvironmentReference, EscalationOutcome, Generation,
|
DomainStore, DomainStoreMetadata, EnvironmentReference, EscalationOutcome, Generation,
|
||||||
GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity, HelperOwnership,
|
GenerationIdentity, GitRoutingOwnership, GuestProcessIdentity, HelperOwnership,
|
||||||
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveSessionBatch,
|
HostProcessIdentity, HostProcessOwnershipSlot, InteractiveProtocolVersion,
|
||||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest, Mailbox,
|
InteractiveSessionBatch, InteractiveSessionBatchV2, InteractiveSessionEvent,
|
||||||
MailboxCancellation, MailboxRequest, MailboxResult, MailboxResultAck, MailboxResultChunk,
|
InteractiveSessionEventV2, InteractiveSessionMessage, InteractiveSessionRequest,
|
||||||
MountIdentity, NamespaceIdentity, NetworkCanaryObservation, NetworkObservation,
|
InteractiveSessionRequestV2, Mailbox, MailboxCancellation, MailboxRequest, MailboxResult,
|
||||||
NetworkOwnership, NetworkReadinessAttestation, NetworkReadinessEvidence,
|
MailboxResultAck, MailboxResultChunk, MountIdentity, NamespaceIdentity,
|
||||||
NetworkReadinessIdentity, NonInteractiveOutput, NonInteractiveSessionMessage,
|
NetworkCanaryObservation, NetworkObservation, NetworkOwnership, NetworkReadinessAttestation,
|
||||||
NonInteractiveSessionRequest, Participant, ParticipantOwnership, ParticipantState,
|
NetworkReadinessEvidence, NetworkReadinessIdentity, NonInteractiveOutput,
|
||||||
PersistedHostProcessOwnership, PidfsProcessAuthorityV1, PidfsProcessIdentityV1, PolicyAction,
|
NonInteractiveSessionMessage, NonInteractiveSessionRequest, Participant, ParticipantOwnership,
|
||||||
PolicyDelivery, PrefixRule, PrepareOperation, PrepareStageState, PreparedArtifact,
|
ParticipantState, PersistedHostProcessOwnership, PidfsProcessAuthorityV1,
|
||||||
PreparedLowerOwnership, ProcessOwnershipCommitAck, ProcessOwnershipMessageVersion,
|
PidfsProcessIdentityV1, PolicyAction, PolicyDelivery, PrefixRule, PrepareOperation,
|
||||||
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, PtyIdentity,
|
PrepareStageState, PreparedArtifact, PreparedLowerOwnership, ProcessOwnershipCommitAck,
|
||||||
PublicationOwnership, ResolvedAutoApprovalRule, ResolvedVolume, Resolver, ResultStream,
|
ProcessOwnershipMessageVersion, ProtocolCredential, ProtocolCredentialRole,
|
||||||
RunscProcessOwnership, SandboxCredentialBundle, SandboxOwnership, Session, SessionBinding,
|
ProtocolPublicationIdentity, PtyIdentity, PublicationOwnership, ResolvedAutoApprovalRule,
|
||||||
SessionClose, SessionExit, SessionExitOutcome, SessionRegistrationRequest,
|
ResolvedVolume, Resolver, ResultStream, RunscProcessOwnership, SandboxCredentialBundle,
|
||||||
SessionRegistrationResponse, SessionRequestCapability, SessionTerminal, SessionTerminalEvent,
|
SandboxOwnership, Session, SessionBinding, SessionClose, SessionExit, SessionExitOutcome,
|
||||||
SessionTerminalMessage, SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership,
|
SessionRegistrationRequest, SessionRegistrationResponse, SessionRequestCapability,
|
||||||
StateAnchorOwnership, StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata,
|
SessionTerminal, SessionTerminalEvent, SessionTerminalEventV2, SessionTerminalMessage,
|
||||||
StoreMetadata, TerminalInput, TerminalOutput, TerminalPrompt, TerminalResize, TerminalSignal,
|
SessionTerminalState, SignedProtocolRecord, StagedProcessOwnership, StateAnchorOwnership,
|
||||||
TerminalSignalKind, TlsCredential, TlsRole, TrustManifest, TrustedHostConfigurationV1,
|
StateAnchorReleaseEvidence, StorageCatalog, StorageCatalogMetadata, StoreMetadata,
|
||||||
|
TerminalBreak, TerminalInput, TerminalModesV2, TerminalOutput, TerminalPrompt, TerminalResize,
|
||||||
|
TerminalSignal, TerminalSignalKind, TlsCredential, TlsRole, TrustManifest,
|
||||||
|
TrustedHostConfigurationV1,
|
||||||
};
|
};
|
||||||
use crate::protocol::{BootstrapWaitOutcome, BootstrapWaitRequest};
|
use crate::protocol::{BootstrapWaitOutcome, BootstrapWaitRequest};
|
||||||
|
|
||||||
|
|
@ -67,16 +70,23 @@ schema_types!(
|
||||||
"ProcessOwnershipCommitAck" => ProcessOwnershipCommitAck,
|
"ProcessOwnershipCommitAck" => ProcessOwnershipCommitAck,
|
||||||
"GuestProcessIdentity" => GuestProcessIdentity,
|
"GuestProcessIdentity" => GuestProcessIdentity,
|
||||||
"SessionTerminalEvent" => SessionTerminalEvent,
|
"SessionTerminalEvent" => SessionTerminalEvent,
|
||||||
|
"SessionTerminalEventV2" => SessionTerminalEventV2,
|
||||||
"TerminalInput" => TerminalInput,
|
"TerminalInput" => TerminalInput,
|
||||||
"TerminalResize" => TerminalResize,
|
"TerminalResize" => TerminalResize,
|
||||||
|
"TerminalModesV2" => TerminalModesV2,
|
||||||
|
"TerminalBreak" => TerminalBreak,
|
||||||
"TerminalSignal" => TerminalSignal,
|
"TerminalSignal" => TerminalSignal,
|
||||||
"TerminalSignalKind" => TerminalSignalKind,
|
"TerminalSignalKind" => TerminalSignalKind,
|
||||||
"SessionClose" => SessionClose,
|
"SessionClose" => SessionClose,
|
||||||
"SessionTerminalMessage" => SessionTerminalMessage,
|
"SessionTerminalMessage" => SessionTerminalMessage,
|
||||||
"InteractiveSessionRequest" => InteractiveSessionRequest,
|
"InteractiveSessionRequest" => InteractiveSessionRequest,
|
||||||
|
"InteractiveProtocolVersion" => InteractiveProtocolVersion,
|
||||||
|
"InteractiveSessionRequestV2" => InteractiveSessionRequestV2,
|
||||||
"InteractiveSessionEvent" => InteractiveSessionEvent,
|
"InteractiveSessionEvent" => InteractiveSessionEvent,
|
||||||
|
"InteractiveSessionEventV2" => InteractiveSessionEventV2,
|
||||||
"InteractiveSessionMessage" => InteractiveSessionMessage,
|
"InteractiveSessionMessage" => InteractiveSessionMessage,
|
||||||
"InteractiveSessionBatch" => InteractiveSessionBatch,
|
"InteractiveSessionBatch" => InteractiveSessionBatch,
|
||||||
|
"InteractiveSessionBatchV2" => InteractiveSessionBatchV2,
|
||||||
"TerminalPrompt" => TerminalPrompt,
|
"TerminalPrompt" => TerminalPrompt,
|
||||||
"NonInteractiveSessionRequest" => NonInteractiveSessionRequest,
|
"NonInteractiveSessionRequest" => NonInteractiveSessionRequest,
|
||||||
"NonInteractiveOutput" => NonInteractiveOutput,
|
"NonInteractiveOutput" => NonInteractiveOutput,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ use crate::identity::{RequestDigest, RequestId};
|
||||||
|
|
||||||
use super::{Deserialize, NonZeroU16, ProtocolVersion, Serialize, SessionId};
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct TerminalInput {
|
pub struct TerminalInput {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
|
|
@ -17,6 +22,34 @@ pub struct TerminalResize {
|
||||||
pub pixel_height: u16,
|
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)]
|
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum TerminalSignalKind {
|
pub enum TerminalSignalKind {
|
||||||
Interrupt,
|
Interrupt,
|
||||||
|
|
@ -35,6 +68,11 @@ pub struct SessionClose {
|
||||||
pub session_id: SessionId,
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum SessionTerminalEvent {
|
pub enum SessionTerminalEvent {
|
||||||
Input(TerminalInput),
|
Input(TerminalInput),
|
||||||
|
|
@ -43,6 +81,14 @@ pub enum SessionTerminalEvent {
|
||||||
Close(SessionClose),
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct TerminalOutput {
|
pub struct TerminalOutput {
|
||||||
pub session_id: SessionId,
|
pub session_id: SessionId,
|
||||||
|
|
@ -147,6 +193,15 @@ pub struct InteractiveSessionRequest {
|
||||||
pub initial_size: TerminalResize,
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct TerminalPrompt {
|
pub struct TerminalPrompt {
|
||||||
pub session_id: SessionId,
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub enum InteractiveSessionMessage {
|
pub enum InteractiveSessionMessage {
|
||||||
Terminal(SessionTerminalMessage),
|
Terminal(SessionTerminalMessage),
|
||||||
|
|
@ -181,6 +248,12 @@ pub struct InteractiveSessionBatch {
|
||||||
pub messages: Vec<InteractiveSessionMessage>,
|
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)]
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||||
pub struct NonInteractiveSessionRequest {
|
pub struct NonInteractiveSessionRequest {
|
||||||
pub protocol_version: super::ProtocolVersion,
|
pub protocol_version: super::ProtocolVersion,
|
||||||
|
|
|
||||||
|
|
@ -288,6 +288,35 @@ fn session_id() -> SessionId {
|
||||||
SessionId::new(opaque(5))
|
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 {
|
fn process(seed: u32) -> PidfsProcessIdentityV1 {
|
||||||
PidfsProcessIdentityV1::new(
|
PidfsProcessIdentityV1::new(
|
||||||
PidfsProcessAuthorityV1 {
|
PidfsProcessAuthorityV1 {
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,12 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
}),
|
}),
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
verify_shape(
|
||||||
|
SessionTerminalEventV2::Break(TerminalBreak {
|
||||||
|
session_id: session_id(),
|
||||||
|
}),
|
||||||
|
&mut seen,
|
||||||
|
);
|
||||||
verify_shape(
|
verify_shape(
|
||||||
TerminalInput {
|
TerminalInput {
|
||||||
session_id: session_id(),
|
session_id: session_id(),
|
||||||
|
|
@ -162,6 +168,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
},
|
},
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
verify_shape(terminal_modes_v2(), &mut seen);
|
||||||
|
verify_shape(
|
||||||
|
TerminalBreak {
|
||||||
|
session_id: session_id(),
|
||||||
|
},
|
||||||
|
&mut seen,
|
||||||
|
);
|
||||||
verify_shape(
|
verify_shape(
|
||||||
TerminalSignal {
|
TerminalSignal {
|
||||||
session_id: session_id(),
|
session_id: session_id(),
|
||||||
|
|
@ -227,12 +240,29 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
},
|
},
|
||||||
&mut seen,
|
&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(
|
verify_shape(
|
||||||
InteractiveSessionEvent::Poll {
|
InteractiveSessionEvent::Poll {
|
||||||
session_id: session_id(),
|
session_id: session_id(),
|
||||||
},
|
},
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
verify_shape(
|
||||||
|
InteractiveSessionEventV2::Poll {
|
||||||
|
session_id: session_id(),
|
||||||
|
},
|
||||||
|
&mut seen,
|
||||||
|
);
|
||||||
let prompt = TerminalPrompt {
|
let prompt = TerminalPrompt {
|
||||||
session_id: session_id(),
|
session_id: session_id(),
|
||||||
request_id: RequestId::new(opaque(54)),
|
request_id: RequestId::new(opaque(54)),
|
||||||
|
|
@ -251,6 +281,13 @@ fn every_schema_shape_round_trips_and_rejects_malformed_or_unknown_versions() {
|
||||||
},
|
},
|
||||||
&mut seen,
|
&mut seen,
|
||||||
);
|
);
|
||||||
|
verify_shape(
|
||||||
|
InteractiveSessionBatchV2 {
|
||||||
|
session_id: session_id(),
|
||||||
|
messages: Vec::new(),
|
||||||
|
},
|
||||||
|
&mut seen,
|
||||||
|
);
|
||||||
verify_shape(generation_identity(), &mut seen);
|
verify_shape(generation_identity(), &mut seen);
|
||||||
verify_shape(trust_manifest(), &mut seen);
|
verify_shape(trust_manifest(), &mut seen);
|
||||||
verify_shape(tls_credential.clone(), &mut seen);
|
verify_shape(tls_credential.clone(), &mut seen);
|
||||||
|
|
@ -649,8 +686,8 @@ persistent_dirs = []
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
seen.digest(),
|
seen.digest(),
|
||||||
[
|
[
|
||||||
54, 187, 13, 149, 227, 157, 1, 54, 234, 66, 127, 9, 34, 65, 173, 20, 214, 200, 126, 67,
|
201, 9, 82, 185, 208, 17, 32, 181, 237, 9, 161, 239, 26, 109, 45, 148, 192, 138, 104,
|
||||||
1, 216, 152, 76, 167, 32, 177, 218, 0, 192, 175, 28,
|
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::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::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::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, {
|
enum_corpus!(digest, ControlResponse, {
|
||||||
ControlResponse::Enrolled(_) => ControlResponse::Enrolled(enrollment_response()),
|
ControlResponse::Enrolled(_) => ControlResponse::Enrolled(enrollment_response()),
|
||||||
|
|
@ -231,6 +232,9 @@ fn credential_corpus(digest: &mut Sha256) {
|
||||||
enum_corpus!(digest, ProtocolVersion, {
|
enum_corpus!(digest, ProtocolVersion, {
|
||||||
ProtocolVersion::V1 => ProtocolVersion::V1,
|
ProtocolVersion::V1 => ProtocolVersion::V1,
|
||||||
});
|
});
|
||||||
|
enum_corpus!(digest, InteractiveProtocolVersion, {
|
||||||
|
InteractiveProtocolVersion::V2 => InteractiveProtocolVersion::V2,
|
||||||
|
});
|
||||||
enum_corpus!(digest, TlsRole, {
|
enum_corpus!(digest, TlsRole, {
|
||||||
TlsRole::SessionCoordinator => TlsRole::SessionCoordinator,
|
TlsRole::SessionCoordinator => TlsRole::SessionCoordinator,
|
||||||
TlsRole::ClientCli => TlsRole::ClientCli,
|
TlsRole::ClientCli => TlsRole::ClientCli,
|
||||||
|
|
@ -325,8 +329,8 @@ fn every_serialized_enum_variant_has_fixed_shape_evidence() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
digest.finalize().as_slice(),
|
digest.finalize().as_slice(),
|
||||||
[
|
[
|
||||||
63, 126, 242, 211, 164, 165, 11, 185, 180, 135, 109, 135, 92, 142, 111, 169, 142, 215,
|
16, 136, 82, 10, 228, 231, 132, 63, 189, 59, 106, 16, 164, 103, 232, 11, 245, 31, 144,
|
||||||
49, 4, 170, 169, 119, 237, 235, 161, 192, 89, 92, 93, 88, 35,
|
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()))?
|
.ok_or_else(|| LifecycleServiceError("committed runtime is absent".into()))?
|
||||||
.stop_sandbox(&sandbox)
|
.stop_sandbox(&sandbox)
|
||||||
.map_err(failure)?;
|
.map_err(failure)?;
|
||||||
|
self.plan
|
||||||
|
.interactive_executor
|
||||||
|
.clean_after_sandbox_absence()
|
||||||
|
.map_err(failure)?;
|
||||||
if self.plan.git_routing.is_some() {
|
if self.plan.git_routing.is_some() {
|
||||||
let git_routing = GitRoutingCoordinator::new(&store);
|
let git_routing = GitRoutingCoordinator::new(&store);
|
||||||
self.runtime
|
self.runtime
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ use std::path::{Path, PathBuf};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard};
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
|
|
||||||
use ed25519_dalek::SigningKey;
|
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_control_client::{ControlClient, ControlClientError};
|
||||||
use lazaret_core::codec::{CodecError, decode, encode};
|
use lazaret_core::codec::{CodecError, decode, encode};
|
||||||
use lazaret_core::configuration::{ConfinedPathPlan, EnvironmentValue};
|
use lazaret_core::configuration::{ConfinedPathPlan, EnvironmentValue};
|
||||||
|
|
@ -24,13 +26,13 @@ use lazaret_core::protocol::{
|
||||||
};
|
};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
|
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
|
||||||
DurableDecision, GenerationIdentity, InteractiveSessionBatch, InteractiveSessionEvent,
|
DurableDecision, GenerationIdentity, InteractiveProtocolVersion, InteractiveSessionBatch,
|
||||||
InteractiveSessionMessage, InteractiveSessionRequest, NonInteractiveSessionMessage,
|
InteractiveSessionBatchV2, InteractiveSessionEventV2, InteractiveSessionMessage,
|
||||||
NonInteractiveSessionRequest, ProtocolCredential, ProtocolCredentialRole,
|
InteractiveSessionRequestV2, NonInteractiveSessionMessage, NonInteractiveSessionRequest,
|
||||||
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, PublicationOwnership,
|
ProtocolCredential, ProtocolCredentialRole, ProtocolPublicationIdentity, ProtocolSubjectId,
|
||||||
SandboxCredentialBundle, Session, SessionBinding, SessionExit, SessionPhase,
|
ProtocolVersion, PublicationOwnership, SandboxCredentialBundle, Session, SessionBinding,
|
||||||
SessionRegistrationRequest, SessionRegistrationResponse, TlsCredential, TlsRole, TlsSubjectId,
|
SessionExit, SessionPhase, SessionRegistrationRequest, SessionRegistrationResponse,
|
||||||
TrustManifest,
|
SessionTerminalMessage, TerminalOutput, TlsCredential, TlsRole, TlsSubjectId, TrustManifest,
|
||||||
};
|
};
|
||||||
use rcgen::{
|
use rcgen::{
|
||||||
BasicConstraints, CertificateParams, CertificateSigningRequestParams, CertifiedIssuer,
|
BasicConstraints, CertificateParams, CertificateSigningRequestParams, CertifiedIssuer,
|
||||||
|
|
@ -354,7 +356,7 @@ impl TrustAnchor {
|
||||||
)],
|
)],
|
||||||
PrivateKeyDer::Pkcs8(self.coordinator_tls_private_key.clone_key()),
|
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.max_early_data_size = 0;
|
||||||
config.send_tls13_tickets = 0;
|
config.send_tls13_tickets = 0;
|
||||||
config.session_storage = Arc::new(NoServerSessionStorage {});
|
config.session_storage = Arc::new(NoServerSessionStorage {});
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use super::{
|
use super::{
|
||||||
Arc, AsFd, BTreeMap, CONTROL_SOCKET_NAME, ContainerId, ControlError, EnvironmentValue, File,
|
Arc, AsFd, BTreeMap, CONTROL_SOCKET_NAME, ContainerId, ControlError, EnvironmentValue, File,
|
||||||
FileIdentity, InteractiveSessionBatch, InteractiveSessionEvent, InteractiveSessionRequest,
|
FileIdentity, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||||
MetadataExt, Mode, NonInteractiveSessionMessage, NonInteractiveSessionRequest, OFlags,
|
InteractiveSessionRequestV2, MetadataExt, Mode, NonInteractiveSessionMessage,
|
||||||
OpenOptions, OsStr, OsString, OsStringExt, PUBLICATION_IDENTITY_NAME, Path, PathBuf,
|
NonInteractiveSessionRequest, OFlags, OpenOptions, OsStr, OsString, OsStringExt,
|
||||||
ProcessExit, ProtocolPublicationIdentity, Read, RunscCommand, STAGED_CONTROL_SOCKET_NAME,
|
PUBLICATION_IDENTITY_NAME, Path, PathBuf, ProcessExit, ProtocolPublicationIdentity, Read,
|
||||||
STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig, SessionExit, SessionId, SessionRegistry,
|
RunscCommand, STAGED_CONTROL_SOCKET_NAME, STAGED_PUBLICATION_IDENTITY_NAME, ServerConfig,
|
||||||
TargetCommand, TrustAnchor, Write, child_broker, fchmod, fmt, io, openat,
|
SessionExit, SessionId, SessionRegistry, TargetCommand, TrustAnchor, Write, child_broker,
|
||||||
remove_if_identity_matches,
|
fchmod, fmt, io, openat, remove_if_identity_matches,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct ControlPaths {
|
pub(super) struct ControlPaths {
|
||||||
|
|
@ -194,8 +194,8 @@ pub trait InteractiveControlSession: Send {
|
||||||
/// Returns an exact-session terminal, prompt-routing, or transport failure.
|
/// Returns an exact-session terminal, prompt-routing, or transport failure.
|
||||||
fn exchange(
|
fn exchange(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: InteractiveSessionEvent,
|
event: InteractiveSessionEventV2,
|
||||||
) -> Result<InteractiveSessionBatch, ControlError>;
|
) -> Result<InteractiveSessionBatchV2, ControlError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait InteractiveSessionExecutor: Send + Sync {
|
pub trait InteractiveSessionExecutor: Send + Sync {
|
||||||
|
|
@ -203,8 +203,12 @@ pub trait InteractiveSessionExecutor: Send + Sync {
|
||||||
/// Returns an acquisition or exact-session identity failure.
|
/// Returns an acquisition or exact-session identity failure.
|
||||||
fn start(
|
fn start(
|
||||||
&self,
|
&self,
|
||||||
request: &InteractiveSessionRequest,
|
request: &InteractiveSessionRequestV2,
|
||||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError>;
|
) -> 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;
|
pub(super) struct InteractiveExecutionUnavailable;
|
||||||
|
|
@ -212,10 +216,14 @@ pub(super) struct InteractiveExecutionUnavailable;
|
||||||
impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
|
impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
|
||||||
fn start(
|
fn start(
|
||||||
&self,
|
&self,
|
||||||
_request: &InteractiveSessionRequest,
|
_request: &InteractiveSessionRequestV2,
|
||||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
||||||
Err(ControlError::InvalidRequest)
|
Err(ControlError::InvalidRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clean_after_sandbox_absence(&self) -> Result<(), ControlError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RunscNonInteractiveExecutor {
|
pub struct RunscNonInteractiveExecutor {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
use super::{
|
use super::{
|
||||||
AcceptedControlConnection, Arc, AsFd, AsRawFd, CONTROL_ALPN, CONTROL_SOCKET_NAME,
|
AcceptedControlConnection, Arc, AsFd, AsRawFd, CONTROL_ALPN, CONTROL_ALPN_V2,
|
||||||
CertificateDigest, CertificateParams, ClientConfig, ClientConnection, ClientEnrollmentResponse,
|
CONTROL_SOCKET_NAME, CertificateDigest, CertificateParams, ClientConfig, ClientConnection,
|
||||||
ClientId, ConfinedPathPlan, ControlClient, ControlError, ControlPaths, ControlRequest,
|
ClientEnrollmentResponse, ClientId, ConfinedPathPlan, ControlClient, ControlError,
|
||||||
ControlResponse, ControlSessionAdmission, Digest, DistinguishedName, DnType, Duration,
|
ControlPaths, ControlRequest, ControlResponse, ControlSessionAdmission, Digest,
|
||||||
ExecutionUnavailable, ExtendedKeyUsagePurpose, File, FileExt, FileIdentity, FileTypeExt,
|
DistinguishedName, DnType, Duration, ExecutionUnavailable, ExtendedKeyUsagePurpose, File,
|
||||||
GenerationIdentity, HostCoordinatorDomainStore, InteractiveSessionBatch,
|
FileExt, FileIdentity, FileTypeExt, GenerationIdentity, HostCoordinatorDomainStore,
|
||||||
InteractiveSessionEvent, InteractiveSessionExecutor, InteractiveSessionMessage, IsCa,
|
InteractiveProtocolVersion, InteractiveSessionBatch, InteractiveSessionBatchV2,
|
||||||
|
InteractiveSessionEventV2, InteractiveSessionExecutor, InteractiveSessionMessage, IsCa,
|
||||||
KeyUsagePurpose, MAX_CONTROL_BYTES, MAX_ENROLLMENT_BYTES, MetadataExt, Mode,
|
KeyUsagePurpose, MAX_CONTROL_BYTES, MAX_ENROLLMENT_BYTES, MetadataExt, Mode,
|
||||||
NonInteractiveSessionExecutor, NonInteractiveSessionMessage, OFlags, OffsetDateTime,
|
NonInteractiveSessionExecutor, NonInteractiveSessionMessage, OFlags, OffsetDateTime,
|
||||||
PUBLICATION_IDENTITY_NAME, Path, PathBuf, PrivateKeyDer, ProtocolPublicationIdentity,
|
PUBLICATION_IDENTITY_NAME, Path, PathBuf, PrivateKeyDer, ProtocolPublicationIdentity,
|
||||||
ProtocolVersion, PublicationOwnership, PublishedControlFace, Read, ServerConnection,
|
ProtocolVersion, PublicationOwnership, PublishedControlFace, Read, ServerConnection,
|
||||||
SessionBinding, SessionId, SessionPhase, SessionRegistrationResponse, SessionRegistry, Sha256,
|
SessionBinding, SessionExit, SessionId, SessionPhase, SessionRegistrationResponse,
|
||||||
SigningKey, StagedControlFace, TlsCredential, TrustAnchor, Write, decode, encode, fmt, io,
|
SessionRegistry, SessionTerminalMessage, Sha256, SigningKey, StagedControlFace, TerminalOutput,
|
||||||
issue_session_capability, openat, publication_recovery_ownership, socket_peercred,
|
TlsCredential, TrustAnchor, Write, decode, encode, fmt, io, issue_session_capability, openat,
|
||||||
|
publication_recovery_ownership, socket_peercred,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl PublishedControlFace {
|
impl PublishedControlFace {
|
||||||
|
|
@ -317,9 +319,11 @@ impl PublishedControlFace {
|
||||||
while connection.is_handshaking() {
|
while connection.is_handshaking() {
|
||||||
connection.complete_io(&mut socket)?;
|
connection.complete_io(&mut socket)?;
|
||||||
}
|
}
|
||||||
if connection.alpn_protocol() != Some(CONTROL_ALPN) {
|
let interactive_v2 = match connection.alpn_protocol() {
|
||||||
return Err(ControlError::InvalidRequest);
|
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| {
|
let peer_certificate = connection.peer_certificates().map(|certificates| {
|
||||||
certificates
|
certificates
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -332,6 +336,7 @@ impl PublishedControlFace {
|
||||||
Some(certificates) => self.serve_registration(
|
Some(certificates) => self.serve_registration(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
&certificates,
|
&certificates,
|
||||||
|
interactive_v2,
|
||||||
&mut admit,
|
&mut admit,
|
||||||
&mut registered,
|
&mut registered,
|
||||||
&mut disconnect,
|
&mut disconnect,
|
||||||
|
|
@ -362,6 +367,7 @@ impl PublishedControlFace {
|
||||||
&self,
|
&self,
|
||||||
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
|
stream: &mut rustls::StreamOwned<ServerConnection, std::os::unix::net::UnixStream>,
|
||||||
certificates: &[Vec<u8>],
|
certificates: &[Vec<u8>],
|
||||||
|
interactive_v2: bool,
|
||||||
admit: &mut Admit,
|
admit: &mut Admit,
|
||||||
registered: &mut Registered,
|
registered: &mut Registered,
|
||||||
disconnect: &mut Disconnect,
|
disconnect: &mut Disconnect,
|
||||||
|
|
@ -410,8 +416,21 @@ impl PublishedControlFace {
|
||||||
}
|
}
|
||||||
Err(error) => return Err(error),
|
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 {
|
match request {
|
||||||
ControlRequest::Execute(request) => {
|
ControlRequest::Execute(request) if !interactive_v2 => {
|
||||||
validate_command(request.session_id, session_id, &request.argv)?;
|
validate_command(request.session_id, session_id, &request.argv)?;
|
||||||
let mut send_message = |message: NonInteractiveSessionMessage| {
|
let mut send_message = |message: NonInteractiveSessionMessage| {
|
||||||
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
||||||
|
|
@ -424,22 +443,46 @@ impl PublishedControlFace {
|
||||||
require_session_id(outcome.session_id(), request.session_id)?;
|
require_session_id(outcome.session_id(), request.session_id)?;
|
||||||
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
||||||
}
|
}
|
||||||
ControlRequest::ExecuteInteractive(request) => {
|
ControlRequest::ExecuteInteractive(request) if !interactive_v2 => {
|
||||||
validate_command(request.session_id, session_id, &request.argv)?;
|
validate_command(request.session_id, session_id, &request.argv)?;
|
||||||
if request.initial_size.session_id != session_id {
|
if request.initial_size.session_id != session_id {
|
||||||
return Err(ControlError::InvalidRequest);
|
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 terminal = self.interactive_executor.start(&request)?;
|
||||||
let mut event = InteractiveSessionEvent::Poll { session_id };
|
let mut event = InteractiveSessionEventV2::Poll { session_id };
|
||||||
loop {
|
loop {
|
||||||
let batch = terminal.exchange(event)?;
|
let batch: InteractiveSessionBatchV2 = terminal.exchange(event)?;
|
||||||
require_interactive_batch(&batch, session_id)?;
|
require_interactive_batch(&batch, session_id)?;
|
||||||
let complete = batch.messages.iter().any(|message| {
|
let complete = batch.messages.iter().any(|message| {
|
||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
InteractiveSessionMessage::Terminal(
|
InteractiveSessionMessage::Terminal(SessionTerminalMessage::Exit(_))
|
||||||
lazaret_core::records::SessionTerminalMessage::Exit(_)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
write_frame(stream, &batch)?;
|
write_frame(stream, &batch)?;
|
||||||
|
|
@ -450,14 +493,13 @@ impl PublishedControlFace {
|
||||||
require_interactive_event(&event, session_id)?;
|
require_interactive_event(&event, session_id)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ControlRequest::Enroll(_) | ControlRequest::Register(_) => {
|
ControlRequest::Execute(_)
|
||||||
Err(ControlError::InvalidRequest)
|
| 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> {
|
fn validate_publication(&self) -> Result<(), ControlError> {
|
||||||
self.paths.validate_root(self.uid)?;
|
self.paths.validate_root(self.uid)?;
|
||||||
|
|
@ -613,24 +655,24 @@ fn require_session_id(actual: SessionId, expected: SessionId) -> Result<(), Cont
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_interactive_event(
|
fn require_interactive_event(
|
||||||
event: &InteractiveSessionEvent,
|
event: &InteractiveSessionEventV2,
|
||||||
expected: SessionId,
|
expected: SessionId,
|
||||||
) -> Result<(), ControlError> {
|
) -> Result<(), ControlError> {
|
||||||
let actual = match event {
|
let actual = match event {
|
||||||
InteractiveSessionEvent::Terminal(event) => match event {
|
InteractiveSessionEventV2::Terminal(event) => match event {
|
||||||
lazaret_core::records::SessionTerminalEvent::Input(event) => event.session_id,
|
lazaret_core::records::SessionTerminalEventV2::Input(event) => event.session_id,
|
||||||
lazaret_core::records::SessionTerminalEvent::Resize(event) => event.session_id,
|
lazaret_core::records::SessionTerminalEventV2::Resize(event) => event.session_id,
|
||||||
lazaret_core::records::SessionTerminalEvent::Signal(event) => event.session_id,
|
lazaret_core::records::SessionTerminalEventV2::Break(event) => event.session_id,
|
||||||
lazaret_core::records::SessionTerminalEvent::Close(event) => event.session_id,
|
lazaret_core::records::SessionTerminalEventV2::Close(event) => event.session_id,
|
||||||
},
|
},
|
||||||
InteractiveSessionEvent::PromptDecision { session_id, .. }
|
InteractiveSessionEventV2::PromptDecision { session_id, .. }
|
||||||
| InteractiveSessionEvent::Poll { session_id } => *session_id,
|
| InteractiveSessionEventV2::Poll { session_id } => *session_id,
|
||||||
};
|
};
|
||||||
require_session_id(actual, expected)
|
require_session_id(actual, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn require_interactive_batch(
|
fn require_interactive_batch(
|
||||||
batch: &InteractiveSessionBatch,
|
batch: &InteractiveSessionBatchV2,
|
||||||
expected: SessionId,
|
expected: SessionId,
|
||||||
) -> Result<(), ControlError> {
|
) -> Result<(), ControlError> {
|
||||||
require_session_id(batch.session_id, expected)?;
|
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());
|
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]
|
#[test]
|
||||||
fn rustls_refuses_server_leaf_with_client_role() {
|
fn rustls_refuses_server_leaf_with_client_role() {
|
||||||
let anchor = TrustAnchor::create(generation()).unwrap();
|
let anchor = TrustAnchor::create(generation()).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,13 @@ use lazaret_core::identity::{
|
||||||
};
|
};
|
||||||
use lazaret_core::protocol::VerifiedSession;
|
use lazaret_core::protocol::VerifiedSession;
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
DeveloperDecision, DomainStore, DurableDecision, GuestProcessIdentity, InteractiveSessionBatch,
|
DeveloperDecision, DomainStore, DurableDecision, GuestProcessIdentity,
|
||||||
InteractiveSessionEvent, InteractiveSessionMessage, InteractiveSessionRequest,
|
InteractiveProtocolVersion, InteractiveSessionBatchV2, InteractiveSessionEventV2,
|
||||||
ParticipantOwnership, ParticipantRole, ParticipantState, PersistedHostProcessOwnership,
|
InteractiveSessionMessage, InteractiveSessionRequestV2, ParticipantOwnership, ParticipantRole,
|
||||||
ProcessOwnershipCommitAck, ProtocolVersion, PtyIdentity, SessionExit, SessionPhase,
|
ParticipantState, PersistedHostProcessOwnership, ProcessOwnershipCommitAck, PtyIdentity,
|
||||||
SessionTerminal, SessionTerminalEvent, SessionTerminalFields, SessionTerminalMessage,
|
SessionExit, SessionPhase, SessionTerminal, SessionTerminalEventV2, SessionTerminalFields,
|
||||||
SessionTerminalState, StagedProcessOwnership, TerminalOutput, TerminalPrompt, TerminalResize,
|
SessionTerminalMessage, SessionTerminalState, StagedProcessOwnership, TerminalModesV2,
|
||||||
TerminalSignalKind,
|
TerminalOutput, TerminalPrompt, TerminalResize,
|
||||||
};
|
};
|
||||||
use lazaret_linux::process::{
|
use lazaret_linux::process::{
|
||||||
ChildStandardIo, ProcessExit, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
ChildStandardIo, ProcessExit, ProcessOwnershipContext, RetainedProcess, TargetCommand,
|
||||||
|
|
@ -29,7 +29,10 @@ use lazaret_linux::process::{
|
||||||
use lazaret_sandbox_contract::SANDBOX_INIT_DESTINATION;
|
use lazaret_sandbox_contract::SANDBOX_INIT_DESTINATION;
|
||||||
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat};
|
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl, fstat};
|
||||||
use rustix::pty::{OpenptFlags, grantpt, ioctl_tiocgptpeer, openpt, unlockpt};
|
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 sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::control::{ControlError, InteractiveControlSession, InteractiveSessionExecutor};
|
use crate::control::{ControlError, InteractiveControlSession, InteractiveSessionExecutor};
|
||||||
|
|
@ -440,6 +443,7 @@ impl PromptRouteCancellation for SessionPromptTransport {
|
||||||
pub trait SessionTerminalProcess: Send {
|
pub trait SessionTerminalProcess: Send {
|
||||||
fn write_input(&mut self, bytes: &[u8]) -> io::Result<usize>;
|
fn write_input(&mut self, bytes: &[u8]) -> io::Result<usize>;
|
||||||
fn resize(&self, resize: TerminalResize) -> io::Result<()>;
|
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 read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize>;
|
||||||
fn exec_exited(&self) -> io::Result<bool>;
|
fn exec_exited(&self) -> io::Result<bool>;
|
||||||
fn signal_group(&self, signal: &str) -> Result<(), TerminalError>;
|
fn signal_group(&self, signal: &str) -> Result<(), TerminalError>;
|
||||||
|
|
@ -471,6 +475,8 @@ pub struct TerminalAcquisition {
|
||||||
pub environment: BTreeMap<String, EnvironmentValue>,
|
pub environment: BTreeMap<String, EnvironmentValue>,
|
||||||
pub internal_pid_file: PathBuf,
|
pub internal_pid_file: PathBuf,
|
||||||
pub argv: Vec<Vec<u8>>,
|
pub argv: Vec<Vec<u8>>,
|
||||||
|
pub initial_size: TerminalResize,
|
||||||
|
pub initial_modes: TerminalModesV2,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HostCoordinatorTerminalRecorder {
|
pub struct HostCoordinatorTerminalRecorder {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,41 @@ use super::{
|
||||||
HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder, Instant, MAX_DRAIN_BYTES,
|
HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder, Instant, MAX_DRAIN_BYTES,
|
||||||
MAX_EVENT_BYTES, MAX_INPUT_QUEUE_BYTES, MAX_OUTPUT_BYTES, POLL_INTERVAL,
|
MAX_EVENT_BYTES, MAX_INPUT_QUEUE_BYTES, MAX_OUTPUT_BYTES, POLL_INTERVAL,
|
||||||
PersistedHostProcessOwnership, ProcessExit, PromptRouteCancellation, SIGNAL_GRACE, SessionExit,
|
PersistedHostProcessOwnership, ProcessExit, PromptRouteCancellation, SIGNAL_GRACE, SessionExit,
|
||||||
SessionId, SessionTerminalEngine, SessionTerminalEvent, SessionTerminalFactory,
|
SessionId, SessionTerminalEngine, SessionTerminalEventV2, SessionTerminalFactory,
|
||||||
SessionTerminalMessage, SessionTerminalProcess, SessionTerminalState, TerminalAcquisition,
|
SessionTerminalMessage, SessionTerminalProcess, SessionTerminalState, TerminalAcquisition,
|
||||||
TerminalError, TerminalLoss, TerminalOutput, TerminalRoute, TerminalSignalKind, VecDeque,
|
TerminalError, TerminalLoss, TerminalOutput, TerminalRoute, VecDeque, clean_terminal,
|
||||||
clean_terminal, copy_terminal_state, io, terminal_snapshot,
|
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 {
|
impl SessionTerminalEngine {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
|
@ -58,6 +87,56 @@ impl SessionTerminalEngine {
|
||||||
Ok(())
|
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
|
/// # Errors
|
||||||
/// Refuses stale, unknown, detached, or releasing Session Terminals.
|
/// Refuses stale, unknown, detached, or releasing Session Terminals.
|
||||||
pub fn set_route(
|
pub fn set_route(
|
||||||
|
|
@ -83,15 +162,15 @@ impl SessionTerminalEngine {
|
||||||
pub fn handle(
|
pub fn handle(
|
||||||
&mut self,
|
&mut self,
|
||||||
generation_id: GenerationId,
|
generation_id: GenerationId,
|
||||||
event: SessionTerminalEvent,
|
event: SessionTerminalEventV2,
|
||||||
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
) -> Result<Vec<SessionTerminalMessage>, TerminalError> {
|
||||||
let session_id = match &event {
|
let session_id = match &event {
|
||||||
SessionTerminalEvent::Input(event) => event.session_id,
|
SessionTerminalEventV2::Input(event) => event.session_id,
|
||||||
SessionTerminalEvent::Resize(event) => event.session_id,
|
SessionTerminalEventV2::Resize(event) => event.session_id,
|
||||||
SessionTerminalEvent::Signal(event) => event.session_id,
|
SessionTerminalEventV2::Break(event) => event.session_id,
|
||||||
SessionTerminalEvent::Close(event) => event.session_id,
|
SessionTerminalEventV2::Close(event) => event.session_id,
|
||||||
};
|
};
|
||||||
if matches!(event, SessionTerminalEvent::Close(_)) {
|
if matches!(event, SessionTerminalEventV2::Close(_)) {
|
||||||
return self.close(
|
return self.close(
|
||||||
session_id,
|
session_id,
|
||||||
generation_id,
|
generation_id,
|
||||||
|
|
@ -112,7 +191,7 @@ impl SessionTerminalEngine {
|
||||||
return Err(TerminalError::InactiveRoute);
|
return Err(TerminalError::InactiveRoute);
|
||||||
}
|
}
|
||||||
match event {
|
match event {
|
||||||
SessionTerminalEvent::Input(input) => {
|
SessionTerminalEventV2::Input(input) => {
|
||||||
if input.bytes.len() > MAX_EVENT_BYTES {
|
if input.bytes.len() > MAX_EVENT_BYTES {
|
||||||
return Err(TerminalError::OversizedInput);
|
return Err(TerminalError::OversizedInput);
|
||||||
}
|
}
|
||||||
|
|
@ -123,20 +202,15 @@ impl SessionTerminalEngine {
|
||||||
flush_input(terminal)?;
|
flush_input(terminal)?;
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
SessionTerminalEvent::Resize(resize) => {
|
SessionTerminalEventV2::Resize(resize) => {
|
||||||
terminal.process.resize(resize)?;
|
terminal.process.resize(resize)?;
|
||||||
terminal.process.signal_group("WINCH")?;
|
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
SessionTerminalEvent::Signal(signal) => {
|
SessionTerminalEventV2::Break(_) => {
|
||||||
terminal.process.signal_group(match signal.signal {
|
terminal.process.send_break()?;
|
||||||
TerminalSignalKind::Interrupt => "INT",
|
|
||||||
TerminalSignalKind::Quit => "QUIT",
|
|
||||||
TerminalSignalKind::Suspend => "TSTP",
|
|
||||||
})?;
|
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
SessionTerminalEvent::Close(_) => unreachable!(),
|
SessionTerminalEventV2::Close(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
use super::{
|
use super::{
|
||||||
Arc, BTreeMap, ContainerId, ControlError, CoordinatorId, DomainId, EnvironmentValue,
|
Arc, BTreeMap, ContainerId, ControlError, CoordinatorId, DomainId, EnvironmentValue,
|
||||||
GenerationId, HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder,
|
GenerationId, HashMap, HostCoordinatorDomainStore, HostCoordinatorTerminalRecorder,
|
||||||
InteractiveControlSession, InteractiveSessionBatch, InteractiveSessionEvent,
|
InteractiveControlSession, InteractiveProtocolVersion, InteractiveSessionBatchV2,
|
||||||
InteractiveSessionExecutor, InteractiveSessionMessage, InteractiveSessionRequest, Mutex,
|
InteractiveSessionEventV2, InteractiveSessionExecutor, InteractiveSessionMessage,
|
||||||
OsString, PathBuf, PidfsProcessIdentityV1, PromptRouteCancellation, ProtocolVersion,
|
InteractiveSessionRequestV2, Mutex, OsString, PathBuf, PidfsProcessIdentityV1,
|
||||||
RunscSessionTerminalFactory, SessionId, SessionPromptTransport, SessionTerminal,
|
PromptRouteCancellation, RunscSessionTerminalFactory, SessionId, SessionPromptTransport,
|
||||||
SessionTerminalEvent, SessionTerminalFactory, SessionTerminalMessage, SessionTerminalProcess,
|
SessionTerminal, SessionTerminalEventV2, SessionTerminalFactory, SessionTerminalMessage,
|
||||||
TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute, VecDeque,
|
SessionTerminalProcess, TerminalAcquisition, TerminalError, TerminalLoss, TerminalRoute,
|
||||||
|
VecDeque,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) struct ActiveTerminal {
|
pub(super) struct ActiveTerminal {
|
||||||
|
|
@ -91,9 +92,9 @@ impl RunscInteractiveSessionExecutor {
|
||||||
impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
||||||
fn start(
|
fn start(
|
||||||
&self,
|
&self,
|
||||||
request: &InteractiveSessionRequest,
|
request: &InteractiveSessionRequestV2,
|
||||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
) -> 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
|
|| request.initial_size.session_id != request.session_id
|
||||||
{
|
{
|
||||||
return Err(ControlError::InvalidRequest);
|
return Err(ControlError::InvalidRequest);
|
||||||
|
|
@ -113,22 +114,13 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
||||||
hex_session(request.session_id.as_bytes())
|
hex_session(request.session_id.as_bytes())
|
||||||
)),
|
)),
|
||||||
argv: request.argv.clone(),
|
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)?;
|
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
|
||||||
engine
|
engine
|
||||||
.acquire(&acquisition)
|
.acquire(&acquisition)
|
||||||
.map_err(|error| terminal_control_error(&error))?;
|
.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);
|
drop(engine);
|
||||||
Ok(Box::new(RunscInteractiveControlSession {
|
Ok(Box::new(RunscInteractiveControlSession {
|
||||||
engine: Arc::clone(&self.engine),
|
engine: Arc::clone(&self.engine),
|
||||||
|
|
@ -137,6 +129,19 @@ impl InteractiveSessionExecutor for RunscInteractiveSessionExecutor {
|
||||||
session_id: request.session_id,
|
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 {
|
pub(super) struct RunscInteractiveControlSession {
|
||||||
|
|
@ -149,11 +154,11 @@ pub(super) struct RunscInteractiveControlSession {
|
||||||
impl InteractiveControlSession for RunscInteractiveControlSession {
|
impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
fn exchange(
|
fn exchange(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: InteractiveSessionEvent,
|
event: InteractiveSessionEventV2,
|
||||||
) -> Result<InteractiveSessionBatch, ControlError> {
|
) -> Result<InteractiveSessionBatchV2, ControlError> {
|
||||||
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
|
let mut engine = self.engine.lock().map_err(|_| ControlError::LockPoisoned)?;
|
||||||
let mut messages = match event {
|
let mut messages = match event {
|
||||||
InteractiveSessionEvent::Terminal(SessionTerminalEvent::Resize(resize)) => {
|
InteractiveSessionEventV2::Terminal(SessionTerminalEventV2::Resize(resize)) => {
|
||||||
if self
|
if self
|
||||||
.prompt_transport
|
.prompt_transport
|
||||||
.retain_prompt_resize(resize)
|
.retain_prompt_resize(resize)
|
||||||
|
|
@ -162,17 +167,17 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
engine
|
engine
|
||||||
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
|
.handle(self.generation_id, SessionTerminalEventV2::Resize(resize))
|
||||||
.map_err(|error| terminal_control_error(&error))?
|
.map_err(|error| terminal_control_error(&error))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InteractiveSessionEvent::Terminal(event) => engine
|
InteractiveSessionEventV2::Terminal(event) => engine
|
||||||
.handle(self.generation_id, event)
|
.handle(self.generation_id, event)
|
||||||
.map_err(|error| terminal_control_error(&error))?,
|
.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()
|
Vec::new()
|
||||||
}
|
}
|
||||||
InteractiveSessionEvent::PromptDecision {
|
InteractiveSessionEventV2::PromptDecision {
|
||||||
session_id,
|
session_id,
|
||||||
signed_decision,
|
signed_decision,
|
||||||
} if session_id == self.session_id => {
|
} if session_id == self.session_id => {
|
||||||
|
|
@ -190,7 +195,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
.map_err(|error| terminal_control_error(&error))?;
|
.map_err(|error| terminal_control_error(&error))?;
|
||||||
if let Some(resize) = decision.resumed_size {
|
if let Some(resize) = decision.resumed_size {
|
||||||
engine
|
engine
|
||||||
.handle(self.generation_id, SessionTerminalEvent::Resize(resize))
|
.handle(self.generation_id, SessionTerminalEventV2::Resize(resize))
|
||||||
.map_err(|error| terminal_control_error(&error))?;
|
.map_err(|error| terminal_control_error(&error))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,8 +204,8 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
.map_err(|error| terminal_control_error(&error))?;
|
.map_err(|error| terminal_control_error(&error))?;
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
InteractiveSessionEvent::PromptDecision { .. }
|
InteractiveSessionEventV2::PromptDecision { .. }
|
||||||
| InteractiveSessionEvent::Poll { .. } => return Err(ControlError::InvalidRequest),
|
| InteractiveSessionEventV2::Poll { .. } => return Err(ControlError::InvalidRequest),
|
||||||
};
|
};
|
||||||
if let Some(prompt) = self
|
if let Some(prompt) = self
|
||||||
.prompt_transport
|
.prompt_transport
|
||||||
|
|
@ -210,7 +215,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
engine
|
engine
|
||||||
.set_route(self.session_id, self.generation_id, TerminalRoute::Prompt)
|
.set_route(self.session_id, self.generation_id, TerminalRoute::Prompt)
|
||||||
.map_err(|error| terminal_control_error(&error))?;
|
.map_err(|error| terminal_control_error(&error))?;
|
||||||
return Ok(InteractiveSessionBatch {
|
return Ok(InteractiveSessionBatchV2 {
|
||||||
session_id: self.session_id,
|
session_id: self.session_id,
|
||||||
messages: vec![InteractiveSessionMessage::Prompt(prompt)],
|
messages: vec![InteractiveSessionMessage::Prompt(prompt)],
|
||||||
});
|
});
|
||||||
|
|
@ -225,7 +230,7 @@ impl InteractiveControlSession for RunscInteractiveControlSession {
|
||||||
.map_err(|error| terminal_control_error(&error))?,
|
.map_err(|error| terminal_control_error(&error))?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(InteractiveSessionBatch {
|
Ok(InteractiveSessionBatchV2 {
|
||||||
session_id: self.session_id,
|
session_id: self.session_id,
|
||||||
messages: messages
|
messages: messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
use super::{
|
use super::{
|
||||||
Arc, ChildStandardIo, DeviceId, File, FileDescriptor, GenerationId, GuestProcessGroupId,
|
Arc, ChildStandardIo, ControlModes, DeviceId, File, FileDescriptor, GenerationId,
|
||||||
GuestProcessId, GuestProcessIdentity, GuestProcessStartTimeTicks,
|
GuestProcessGroupId, GuestProcessId, GuestProcessIdentity, GuestProcessStartTimeTicks,
|
||||||
HostCoordinatorTerminalRecorder, IDENTITY_DEADLINE, InodeId, Instant, Mutex, OFlags,
|
HostCoordinatorTerminalRecorder, IDENTITY_DEADLINE, InodeId, InputModes, Instant, LocalModes,
|
||||||
OpenptFlags, OsString, POLL_INTERVAL, PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE,
|
Mutex, OFlags, OpenptFlags, OptionalActions, OsString, OutputModes, POLL_INTERVAL,
|
||||||
PersistedHostProcessOwnership, PidfsProcessIdentityV1, ProcessExit, ProcessOwnershipCommitAck,
|
PRIVATE_SESSION_EXEC_ROLE, PRIVATE_SESSION_IDENTITY_ROLE, PersistedHostProcessOwnership,
|
||||||
ProcessOwnershipContext, PtyIdentity, RetainedProcess, SANDBOX_INIT_DESTINATION, SessionId,
|
PidfsProcessIdentityV1, ProcessExit, ProcessOwnershipCommitAck, ProcessOwnershipContext,
|
||||||
SessionTerminal, SessionTerminalFactory, SessionTerminalFields, SessionTerminalProcess,
|
PtyIdentity, RetainedProcess, SANDBOX_INIT_DESTINATION, SessionId, SessionTerminal,
|
||||||
SessionTerminalState, StagedProcessOwnership, TargetCommand, TerminalAcquisition,
|
SessionTerminalFactory, SessionTerminalFields, SessionTerminalProcess, SessionTerminalState,
|
||||||
TerminalError, TerminalResize, Winsize, child_broker, current_process_identity, fcntl_getfl,
|
SpecialCodeIndex, StagedProcessOwnership, TargetCommand, TerminalAcquisition, TerminalError,
|
||||||
fcntl_setfl, fstat, grantpt, io, ioctl_tiocgptpeer, openpt, release_physical, tcsetwinsize,
|
TerminalModesV2, TerminalResize, Winsize, child_broker, current_process_identity, fcntl_getfl,
|
||||||
unlockpt,
|
fcntl_setfl, fstat, grantpt, io, ioctl_tiocgptpeer, openpt, release_physical, tcgetattr,
|
||||||
|
tcsendbreak, tcsetattr, tcsetwinsize, unlockpt,
|
||||||
};
|
};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
|
|
@ -40,6 +41,12 @@ impl SessionTerminalFactory for RunscSessionTerminalFactory {
|
||||||
return Err(TerminalError::AuthorityMismatch);
|
return Err(TerminalError::AuthorityMismatch);
|
||||||
}
|
}
|
||||||
let (master, slave, pty) = allocate_pty()?;
|
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)?;
|
remove_pid_file(&request.internal_pid_file)?;
|
||||||
let mut target = TargetCommand::new(request.runsc_program.clone());
|
let mut target = TargetCommand::new(request.runsc_program.clone());
|
||||||
for argument in &request.runsc_global_arguments {
|
for argument in &request.runsc_global_arguments {
|
||||||
|
|
@ -308,6 +315,15 @@ impl SessionTerminalProcess for RunscSessionTerminal {
|
||||||
Ok(())
|
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> {
|
fn read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||||
self.master
|
self.master
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
|
@ -338,12 +354,13 @@ impl SessionTerminalProcess for RunscSessionTerminal {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn group_absent(&self) -> Result<bool, TerminalError> {
|
fn group_absent(&self) -> Result<bool, TerminalError> {
|
||||||
if observe_guest_identity(&self.request, self.guest.leader_pid.get())?
|
let leader_matches = observe_guest_identity(&self.request, self.guest.leader_pid.get())?
|
||||||
.is_some_and(|observed| observed != self.guest)
|
.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);
|
return Err(TerminalError::GuestIdentityMismatch);
|
||||||
}
|
}
|
||||||
observe_group_absent(&self.request, self.guest.process_group_id.get())
|
Ok(group_absent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn terminate_exec(&self) -> io::Result<()> {
|
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))
|
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(
|
pub(super) fn staged_terminal(
|
||||||
request: &TerminalAcquisition,
|
request: &TerminalAcquisition,
|
||||||
pty: PtyIdentity,
|
pty: PtyIdentity,
|
||||||
|
|
@ -619,3 +690,71 @@ fn hex_lower(bytes: &[u8]) -> String {
|
||||||
output
|
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::protocol::{VerifiedSession, encode_signed, issue_session_capability};
|
||||||
use lazaret_core::records::{
|
use lazaret_core::records::{
|
||||||
DeveloperDecisionKind, Domain, DurableDecision, Generation, GenerationIdentity, MailboxRequest,
|
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 lazaret_linux::filesystem::try_exclusive_flock;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
@ -352,6 +353,17 @@ impl SessionTerminalProcess for MockProcess {
|
||||||
Ok(())
|
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> {
|
fn read_output(&mut self, bytes: &mut [u8]) -> io::Result<usize> {
|
||||||
let mut state = self.state.lock().unwrap();
|
let mut state = self.state.lock().unwrap();
|
||||||
let Some(output) = state.output.pop_front() else {
|
let Some(output) = state.output.pop_front() else {
|
||||||
|
|
@ -399,7 +411,7 @@ impl SessionTerminalProcess for MockProcess {
|
||||||
if let Some(hook) = hook {
|
if let Some(hook) = hook {
|
||||||
hook();
|
hook();
|
||||||
}
|
}
|
||||||
if identity_mismatch {
|
if identity_mismatch && !group_absent {
|
||||||
return Err(TerminalError::GuestIdentityMismatch);
|
return Err(TerminalError::GuestIdentityMismatch);
|
||||||
}
|
}
|
||||||
Ok(group_absent)
|
Ok(group_absent)
|
||||||
|
|
@ -529,6 +541,43 @@ fn acquisition(session_id: SessionId) -> TerminalAcquisition {
|
||||||
environment: BTreeMap::new(),
|
environment: BTreeMap::new(),
|
||||||
internal_pid_file: "/tmp/not-used-by-mock".into(),
|
internal_pid_file: "/tmp/not-used-by-mock".into(),
|
||||||
argv: vec![b"/bin/agent".to_vec()],
|
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